dev-login.mjs 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. /**
  2. * 开发态自动登录(**仅本地静态服务**,绝不进入应用包)
  3. *
  4. * ## 它解决什么
  5. *
  6. * 本地跑生产构建(`npm run build` + 静态服务)时,登录要人工点「Token 直登」并粘贴
  7. * 一长串 `r:` Token —— 每建一个浏览器上下文就要重复一次,Playwright / 冒烟 /
  8. * 真实账号回归全都卡在这一步,无法自动化。
  9. *
  10. * ## 它怎么工作
  11. *
  12. * 本模块由 `scripts/dev-server.mjs` 进程通过 `import()` 加载,把一段**内联脚本**
  13. * 注入 `index.html` 的 `<head>`。那段脚本唯一做的事:在 Angular 启动前,把
  14. * `.claude/dev-login.json` 里的 token / me 写进 localStorage(键名与
  15. * `SessionService` 完全一致:`fmode_session_token` / `fmode_me`)。
  16. *
  17. * · 不在 `src/` 下 → 不在任何 tsconfig 的输入里 → **不会被打进应用构建**
  18. * · 不写进 `index.html` 源文件 → 仓库里的 `index.html` 保持干净
  19. * · 只有 `scripts/dev-server.mjs` 会调用 → 生产部署链路完全不知道它存在
  20. *
  21. * ## 为什么不放在环境文件里
  22. *
  23. * `environment*.ts` 会被编译进包,`ng build` 之后 token 就固定在产物里了。
  24. * 本模块走运行时注入,token 改了只需刷新页面,且产物始终是干净的生产包。
  25. */
  26. import { readFileSync, existsSync } from 'node:fs';
  27. import { resolve, dirname } from 'node:path';
  28. import { fileURLToPath } from 'node:url';
  29. const here = dirname(fileURLToPath(import.meta.url));
  30. // 本文件在 scripts/ 下;.claude/dev-login.json 在**项目根**下,故上溯一级
  31. const CONFIG_PATH = resolve(here, '../.claude/dev-login.json');
  32. export const DEV_LOGIN_KEYS = { TOKEN: 'fmode_session_token', ME: 'fmode_me' };
  33. /**
  34. * 读 `.claude/dev-login.json`。
  35. *
  36. * @returns {{ enabled: boolean, reason?: string, payload?: { token: string, me: object } }}
  37. */
  38. export function loadDevLogin() {
  39. if (!existsSync(CONFIG_PATH)) {
  40. return { enabled: false, reason: `未找到 ${CONFIG_PATH}` };
  41. }
  42. let raw;
  43. try {
  44. raw = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
  45. } catch (e) {
  46. return { enabled: false, reason: `dev-login.json 解析失败:${e.message}` };
  47. }
  48. const token = typeof raw.token === 'string' ? raw.token.trim() : '';
  49. const me = raw.me && typeof raw.me === 'object' ? raw.me : null;
  50. if (!token) return { enabled: false, reason: 'dev-login.json 的 token 为空' };
  51. if (!me || !me.id) return { enabled: false, reason: 'dev-login.json 的 me.id 为空' };
  52. return { enabled: true, payload: { token, me } };
  53. }
  54. /**
  55. * 生成注入用的内联脚本。
  56. *
  57. * 🔴 用 `JSON.stringify` 而不是模板拼接 —— 值里的引号/换行会被正确转义。
  58. * 🔴 但 `JSON.stringify` **不会**转义 `<` / `>` —— 值里若含 `</script>` 会提前闭合
  59. * 标签,把后面的内容当成 HTML 执行(实测踩到)。所以额外把 `<` `>` `&`
  60. * 转成 `<` / `>` / `&`(JSON 里等价,且无法闭合标签)。
  61. */
  62. export function buildInjectionScript(payload) {
  63. const { TOKEN, ME } = DEV_LOGIN_KEYS;
  64. const data = JSON.stringify({ key: TOKEN, meKey: ME, token: payload.token, me: payload.me })
  65. .replace(/</g, '\\u003c')
  66. .replace(/>/g, '\\u003e')
  67. .replace(/&/g, '\\u0026');
  68. return `<script>(function(){try{var d=${data};` +
  69. `if(!localStorage.getItem(d.key)){localStorage.setItem(d.key,d.token);` +
  70. `localStorage.setItem(d.meKey,JSON.stringify(d.me));}}catch(e){}})();</script>`;
  71. }
  72. /**
  73. * 把注入脚本插到 `</head>` 前。幂等:已注入过就原样返回。
  74. */
  75. export function injectIntoHtml(html, payload) {
  76. const marker = '<!-- dev-login -->';
  77. if (html.includes(marker)) return html;
  78. const script = marker + buildInjectionScript(payload);
  79. const i = html.toLowerCase().indexOf('</head>');
  80. if (i < 0) return script + html;
  81. return html.slice(0, i) + script + html.slice(i);
  82. }