cc-viewer 1.6.322 → 1.6.324
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +38 -0
- package/dist/assets/App-DxhvxOv5.js +2 -0
- package/dist/assets/{MdxEditorPanel-CZz9w38A.js → MdxEditorPanel-Cvce-OZ1.js} +1 -1
- package/dist/assets/Mobile-eQkx6wDh.js +1 -0
- package/dist/assets/{_baseUniq-CKn860hN.js → _baseUniq-Bd-sKzQW.js} +1 -1
- package/dist/assets/{arc-C4-SwDEU.js → arc-D_6rT0rI.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-zNU8vGgf.js → architectureDiagram-Q4EWVU46-ClH0N8bK.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Blq7VyAY.js → blockDiagram-DXYQGD6D-VBVbHV93.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CilEO7Pu.js → c4Diagram-AHTNJAMY-CHd3FVhr.js} +1 -1
- package/dist/assets/{channel-K9LDJtR8.js → channel-DtAIDDMS.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-DB6nsrqZ.js → chunk-4BX2VUAB-ldBMt7Zz.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-O_q60rT5.js → chunk-4TB4RGXK-BpaavRFi.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-D4PJNgqD.js → chunk-55IACEB6-CCQR_GqB.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-T78XNZnN.js → chunk-EDXVE4YY-B94gGHJo.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BMajr_WJ.js → chunk-FMBD7UC4-D2pvMCHW.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-BLXJEzI0.js → chunk-OYMX7WX6-BC1K8PFw.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-C4ozCDaC.js → chunk-QZHKN3VN-BlKIHWPg.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-xsMRpY5W.js → chunk-YZCP3GAM-CO46Dpuq.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BVr-1vS2.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-BVr-1vS2.js +1 -0
- package/dist/assets/clone-BLbDEtop.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BRm6sjuX.js → cose-bilkent-S5V4N54A-UW2sc3aR.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-CapXy71H.js → dagre-KV5264BT-nawhI5CM.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-CHaUXNHL.js → diagram-5BDNPKRD-BrLDz-85.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-YInXouX7.js → diagram-G4DWMVQ6-DDT86i03.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-Bb9563xL.js → diagram-MMDJMWI5-BQMzmHLU.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-BRYSGtnv.js → diagram-TYMM5635-BX7-PwHQ.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-DiYsrHLg.js → erDiagram-SMLLAGMA-IwLVLNv0.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CNZRDRgn.js → flowDiagram-DWJPFMVM-B8_X77WK.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-D9QY8NPb.js → ganttDiagram-T4ZO3ILL-DgeHpfKD.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-R_yUkND4.js → gitGraphDiagram-UUTBAWPF-GImqd44T.js} +1 -1
- package/dist/assets/{graph-Bp7rzoPN.js → graph-BFNvFrIw.js} +1 -1
- package/dist/assets/{index-C2X1QxuP.js → index-CG43hSOO.js} +1 -1
- package/dist/assets/{index-skAVyRFx.js → index-CL43Q_T6.js} +1 -1
- package/dist/assets/index-Cw_igOCh.js +2 -0
- package/dist/assets/{index-5ATcFzQK.js → index-DH6neeAn.js} +1 -1
- package/dist/assets/{index-FjGT9Yrp.js → index-DYZfyIFS.js} +1 -1
- package/dist/assets/{index-CQm1mKh-.js → index-DqxWEMZG.js} +1 -1
- package/dist/assets/{index-B6uEt4Lm.js → index-SMWE-umF.js} +1 -1
- package/dist/assets/{index-D7TrcLgR.js → index-Zcwywgin.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-D7SIo5T_.js → infoDiagram-42DDH7IO-DafQ4Zwr.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-D1w7_hBw.js → ishikawaDiagram-UXIWVN3A-CsS59UI8.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-D2ZcdIwf.js → journeyDiagram-VCZTEJTY-C8OZZC2d.js} +1 -1
- package/dist/assets/{jszip.min-BhyPZTcl.js → jszip.min-DWTs-GDW.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-BtXrBplk.js → kanban-definition-6JOO6SKY-BThryIIr.js} +1 -1
- package/dist/assets/{layout-l4xJ0EnV.js → layout-B2c4ocGM.js} +1 -1
- package/dist/assets/{linear-BFgL1PM1.js → linear-whLrOj6w.js} +1 -1
- package/dist/assets/{mermaid.core-CBJGZX-R.js → mermaid.core-CVxcxPSX.js} +2 -2
- package/dist/assets/{min-D7RE9cIU.js → min-DcYONCON.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-B2jur7ry.js → mindmap-definition-QFDTVHPH-B7cxL7F7.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-B249cLkg.js → pieDiagram-DEJITSTG-C5W3T882.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-CdZHycc8.js → quadrantDiagram-34T5L4WZ-DOZS2aRU.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-BezSNLdn.js → requirementDiagram-MS252O5E-C_ZtyqTm.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-C-RZnw4b.js → sankeyDiagram-XADWPNL6-DImjFcUx.js} +1 -1
- package/dist/assets/{seqResourceLoaders-R12WGANd.css → seqResourceLoaders-D6YenrBg.css} +1 -1
- package/dist/assets/seqResourceLoaders-sKqNZgOI.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-Fs0rcjkY.js → sequenceDiagram-FGHM5R23-BQwGiH06.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-CIcdyOPX.js → stateDiagram-FHFEXIEX-D8zGNMAW.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BK2tAdaI.js → stateDiagram-v2-QKLJ7IA2-DY86eLVa.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-BXg7hZyc.js → timeline-definition-GMOUNBTQ-LfDkNw95.js} +1 -1
- package/dist/assets/{vendor-antd-BSgMQtAG.js → vendor-antd-x2KtHa4R.js} +1 -1
- package/dist/assets/{vendor-codemirror-rm3PGybQ.js → vendor-codemirror-z6VSi4ab.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-UIfRXbTk.js → vendor-mdxeditor-Zi3mchEB.js} +2 -2
- package/dist/assets/{vendor-qrcode-BiBG2gg5.js → vendor-qrcode-Dtu30dr9.js} +1 -1
- package/dist/assets/{vendor-virtuoso-DNNzcVZ0.js → vendor-virtuoso-BhHaLmft.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-IyEu5-2i.js → vennDiagram-DHZGUBPP-DRRBdcoR.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-BEUuSlqC.js → wardley-RL74JXVD-DtWPL3Ou.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Cv-nt0oQ.js → wardleyDiagram-NUSXRM2D-CDGkjbzC.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-B4QuJzX1.js → xychartDiagram-5P7HB3ND-drrUfgKK.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +4 -4
- package/server/i18n.js +62 -2
- package/server/interceptor.js +32 -9
- package/server/lib/auth.js +20 -5
- package/server/lib/instance-registry.js +57 -0
- package/server/lib/interceptor-core.js +53 -5
- package/server/lib/log-management.js +57 -25
- package/server/lib/prefs-store.js +83 -0
- package/server/lib/project-prefs.js +94 -0
- package/server/lib/session-pin-store.js +67 -0
- package/server/routes/logs.js +5 -2
- package/server/routes/preferences.js +62 -27
- package/server/routes/project-meta.js +3 -2
- package/server/routes/project-prefs.js +132 -0
- package/server/routes/session-pin.js +41 -0
- package/server/server.js +21 -0
- package/dist/assets/App-CafPuJo7.js +0 -2
- package/dist/assets/Mobile-CA6pLj7l.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BHoVlsUt.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BHoVlsUt.js +0 -1
- package/dist/assets/clone-BdK-5KXX.js +0 -1
- package/dist/assets/index-BPWe3-MC.js +0 -2
- package/dist/assets/seqResourceLoaders-BZN9aWp8.js +0 -2
package/server/i18n.js
CHANGED
|
@@ -223,8 +223,8 @@ const i18nData = {
|
|
|
223
223
|
"uk": "\nДля видалення виконайте: ccv --uninstall"
|
|
224
224
|
},
|
|
225
225
|
"cli.help": {
|
|
226
|
-
"zh": "CC Viewer CLI — Claude Code 的 Web Viewer 前端\n\n用法: ccv [选项] [命令] [提示词]\n\n默认启动交互式会话 + Web Viewer,所有参数透传给 claude。\n\n参数:\n prompt 你的提示词\n\nCCV 专属选项:\n -SDK, --sdk 使用 Agent SDK 模式(无终端,结构化交互)\n -logger 安装/修复 Claude Code Hook\n --uninstall 移除 CC Viewer 集成\n --d --dangerously-skip-permissions 的快捷方式\n --ad --allow-dangerously-skip-permissions 的快捷方式\n --log-dir <path> 自定义 JSONL 日志存储目录\n --no-open 启动时不自动打开浏览器\n --user-name <name> 自定义显示名称\n --user-avatar <path|url> 自定义头像图片(文件路径或 URL)\n --usePassword[=<pwd>] 启用密码登录(远程访问需密码或 token;不带值则随机生成 6 位(2 字母 + 4 数字))\n\nClaude 选项(透传):\n -c, --continue 继续当前目录最近的对话\n -r, --resume [value] 通过会话 ID 恢复对话,或打开交互式选择器\n -p, --print 打印响应后退出(适用于管道)\n -d, --debug [filter] 启用调试模式\n -w, --worktree [name] 为此会话创建新的 git worktree\n --model <model> 当前会话使用的模型\n --effort <level> 努力级别 (low, medium, high, max)\n --dangerously-skip-permissions 跳过所有权限检查\n --allowedTools <tools...> 允许的工具列表\n --disallowedTools <tools...> 禁止的工具列表\n --system-prompt <prompt> 会话系统提示词\n --append-system-prompt <prompt> 追加系统提示词\n --mcp-config <configs...> 加载 MCP 服务器配置\n --permission-mode <mode> 权限模式\n --add-dir <directories...> 额外允许访问的目录\n --max-budget-usd <amount> API 调用最大预算(仅 --print)\n --output-format <format> 输出格式(仅 --print): text, json, stream-json\n --json-schema <schema> 结构化输出的 JSON Schema\n --verbose 详细模式\n -h, --help 显示帮助\n -v, --version 显示版本\n\n 更多选项请参考: claude --help\n\n示例:\n ccv 启动交互模式\n ccv -c 继续上次对话\n ccv -r 恢复对话\n ccv -p \"hello\" 打印模式\n ccv --model opus 指定模型\n ccv --d 跳过权限确认(快捷方式)\n ccv --ad 允许跳过权限(不立即启用,可通过 Shift+Tab 切换)\n ccv -c --model sonnet --effort max 组合使用\n ccv -SDK Agent SDK 模式(无终端)\n ccv -SDK --d SDK 模式 + 跳过权限\n ccv -logger 安装/修复 Hook\n ccv --log-dir ~/my-logs 使用自定义日志目录\n ccv --no-open 启动时不打开浏览器\n ccv --user-name \"Alice\" --user-avatar ./avatar.png 自定义用户身份\n ccv --log-dir /tmp/test --no-open 组合 CCV 选项",
|
|
227
|
-
"en": "CC Viewer CLI — Web Viewer frontend for Claude Code\n\nUsage: ccv [options] [command] [prompt]\n\nStarts an interactive session with Web Viewer by default. All args are passed\nthrough to claude.\n\nArguments:\n prompt Your prompt\n\nCCV Options:\n -SDK, --sdk Use Agent SDK mode (no terminal, structured interaction)\n -logger Install/repair Claude Code hook\n --uninstall Remove CC Viewer integration\n --d Shortcut for --dangerously-skip-permissions\n --ad Shortcut for --allow-dangerously-skip-permissions\n --log-dir <path> Custom JSONL log storage directory\n --no-open Don't auto-open browser on startup\n --user-name <name> Custom display name for this session\n --user-avatar <path|url> Custom avatar image (file path or URL)\n --usePassword[=<pwd>] Enable password login (remote needs password or token; bare flag → random 6-char: 2 letters + 4 digits)\n\nClaude Options (passed through):\n -c, --continue Continue the most recent conversation\n -r, --resume [value] Resume a conversation by session ID, or open picker\n -p, --print Print response and exit (useful for pipes)\n -d, --debug [filter] Enable debug mode\n -w, --worktree [name] Create a new git worktree for this session\n --model <model> Model for the current session\n --effort <level> Effort level (low, medium, high, max)\n --dangerously-skip-permissions Bypass all permission checks\n --allowedTools <tools...> Allowed tool names\n --disallowedTools <tools...> Disallowed tool names\n --system-prompt <prompt> System prompt for the session\n --append-system-prompt <prompt> Append to default system prompt\n --mcp-config <configs...> Load MCP servers from JSON files or strings\n --permission-mode <mode> Permission mode for the session\n --add-dir <directories...> Additional directories to allow access to\n --max-budget-usd <amount> Max dollar amount for API calls (--print only)\n --output-format <format> Output format (--print only): text, json, stream-json\n --json-schema <schema> JSON Schema for structured output\n --verbose Enable verbose mode\n -h, --help Show help\n -v, --version Show version\n\n For all options, see: claude --help\n\nExamples:\n ccv Start interactive mode\n ccv -c Continue last conversation\n ccv -r Resume a conversation\n ccv -p \"hello\" Print mode\n ccv --model opus Specify model\n ccv --d Skip permission prompts (shortcut)\n ccv --ad Allow bypass mode (not active, toggle via Shift+Tab)\n ccv -c --model sonnet --effort max Combine options\n ccv -SDK Agent SDK mode (no terminal)\n ccv -SDK --d SDK mode + bypass permissions\n ccv -logger Install/repair hook\n ccv --log-dir ~/my-logs Use custom log directory\n ccv --no-open Start without opening browser\n ccv --user-name \"Alice\" --user-avatar ./avatar.png Custom user profile\n ccv --log-dir /tmp/test --no-open Combine CCV options",
|
|
226
|
+
"zh": "CC Viewer CLI — Claude Code 的 Web Viewer 前端\n\n用法: ccv [选项] [命令] [提示词]\n\n默认启动交互式会话 + Web Viewer,所有参数透传给 claude。\n\n参数:\n prompt 你的提示词\n\nCCV 专属选项:\n -SDK, --sdk 使用 Agent SDK 模式(无终端,结构化交互)\n -logger 安装/修复 Claude Code Hook\n --uninstall 移除 CC Viewer 集成\n --d --dangerously-skip-permissions 的快捷方式\n --ad --allow-dangerously-skip-permissions 的快捷方式\n --log-dir <path> 自定义 JSONL 日志存储目录\n --no-open 启动时不自动打开浏览器\n --user-name <name> 自定义显示名称\n --user-avatar <path|url> 自定义头像图片(文件路径或 URL)\n --usePassword[=<pwd>] 启用密码登录(远程访问需密码或 token;不带值则随机生成 6 位(2 字母 + 4 数字))\n --pid[=<name>] 锁定实例 id:让本 ccv 的「仅展示当前会话」视图按实例隔离(下次用同一 id 复用)\n\nClaude 选项(透传):\n -c, --continue 继续当前目录最近的对话\n -r, --resume [value] 通过会话 ID 恢复对话,或打开交互式选择器\n -p, --print 打印响应后退出(适用于管道)\n -d, --debug [filter] 启用调试模式\n -w, --worktree [name] 为此会话创建新的 git worktree\n --model <model> 当前会话使用的模型\n --effort <level> 努力级别 (low, medium, high, max)\n --dangerously-skip-permissions 跳过所有权限检查\n --allowedTools <tools...> 允许的工具列表\n --disallowedTools <tools...> 禁止的工具列表\n --system-prompt <prompt> 会话系统提示词\n --append-system-prompt <prompt> 追加系统提示词\n --mcp-config <configs...> 加载 MCP 服务器配置\n --permission-mode <mode> 权限模式\n --add-dir <directories...> 额外允许访问的目录\n --max-budget-usd <amount> API 调用最大预算(仅 --print)\n --output-format <format> 输出格式(仅 --print): text, json, stream-json\n --json-schema <schema> 结构化输出的 JSON Schema\n --verbose 详细模式\n -h, --help 显示帮助\n -v, --version 显示版本\n\n 更多选项请参考: claude --help\n\n示例:\n ccv 启动交互模式\n ccv -c 继续上次对话\n ccv -r 恢复对话\n ccv -p \"hello\" 打印模式\n ccv --model opus 指定模型\n ccv --d 跳过权限确认(快捷方式)\n ccv --ad 允许跳过权限(不立即启用,可通过 Shift+Tab 切换)\n ccv -c --model sonnet --effort max 组合使用\n ccv -SDK Agent SDK 模式(无终端)\n ccv -SDK --d SDK 模式 + 跳过权限\n ccv -logger 安装/修复 Hook\n ccv --log-dir ~/my-logs 使用自定义日志目录\n ccv --no-open 启动时不打开浏览器\n ccv --user-name \"Alice\" --user-avatar ./avatar.png 自定义用户身份\n ccv --log-dir /tmp/test --no-open 组合 CCV 选项",
|
|
227
|
+
"en": "CC Viewer CLI — Web Viewer frontend for Claude Code\n\nUsage: ccv [options] [command] [prompt]\n\nStarts an interactive session with Web Viewer by default. All args are passed\nthrough to claude.\n\nArguments:\n prompt Your prompt\n\nCCV Options:\n -SDK, --sdk Use Agent SDK mode (no terminal, structured interaction)\n -logger Install/repair Claude Code hook\n --uninstall Remove CC Viewer integration\n --d Shortcut for --dangerously-skip-permissions\n --ad Shortcut for --allow-dangerously-skip-permissions\n --log-dir <path> Custom JSONL log storage directory\n --no-open Don't auto-open browser on startup\n --user-name <name> Custom display name for this session\n --user-avatar <path|url> Custom avatar image (file path or URL)\n --usePassword[=<pwd>] Enable password login (remote needs password or token; bare flag → random 6-char: 2 letters + 4 digits)\n --pid[=<name>] Lock an instance id so this ccv's current-session view is isolated per instance (reuse the same id next run)\n\nClaude Options (passed through):\n -c, --continue Continue the most recent conversation\n -r, --resume [value] Resume a conversation by session ID, or open picker\n -p, --print Print response and exit (useful for pipes)\n -d, --debug [filter] Enable debug mode\n -w, --worktree [name] Create a new git worktree for this session\n --model <model> Model for the current session\n --effort <level> Effort level (low, medium, high, max)\n --dangerously-skip-permissions Bypass all permission checks\n --allowedTools <tools...> Allowed tool names\n --disallowedTools <tools...> Disallowed tool names\n --system-prompt <prompt> System prompt for the session\n --append-system-prompt <prompt> Append to default system prompt\n --mcp-config <configs...> Load MCP servers from JSON files or strings\n --permission-mode <mode> Permission mode for the session\n --add-dir <directories...> Additional directories to allow access to\n --max-budget-usd <amount> Max dollar amount for API calls (--print only)\n --output-format <format> Output format (--print only): text, json, stream-json\n --json-schema <schema> JSON Schema for structured output\n --verbose Enable verbose mode\n -h, --help Show help\n -v, --version Show version\n\n For all options, see: claude --help\n\nExamples:\n ccv Start interactive mode\n ccv -c Continue last conversation\n ccv -r Resume a conversation\n ccv -p \"hello\" Print mode\n ccv --model opus Specify model\n ccv --d Skip permission prompts (shortcut)\n ccv --ad Allow bypass mode (not active, toggle via Shift+Tab)\n ccv -c --model sonnet --effort max Combine options\n ccv -SDK Agent SDK mode (no terminal)\n ccv -SDK --d SDK mode + bypass permissions\n ccv -logger Install/repair hook\n ccv --log-dir ~/my-logs Use custom log directory\n ccv --no-open Start without opening browser\n ccv --user-name \"Alice\" --user-avatar ./avatar.png Custom user profile\n ccv --log-dir /tmp/test --no-open Combine CCV options",
|
|
228
228
|
"zh-TW": "CC Viewer CLI — Claude Code 的 Web Viewer 前端\n\n用法: ccv [選項] [命令] [提示詞]\n\n預設啟動互動式會話 + Web Viewer,所有參數透傳給 claude。\n\n參數:\n prompt 你的提示詞\n\nCCV 專屬選項:\n -SDK, --sdk 使用 Agent SDK 模式(無終端,結構化互動)\n -logger 安裝/修復 Claude Code Hook\n --uninstall 移除 CC Viewer 整合\n --d --dangerously-skip-permissions 的快捷方式\n --ad --allow-dangerously-skip-permissions 的快捷方式\n --log-dir <path> 自訂 JSONL 日誌儲存目錄\n --no-open 啟動時不自動開啟瀏覽器\n --user-name <name> 自訂顯示名稱\n --user-avatar <path|url> 自訂頭像圖片(檔案路徑或 URL)\n\nClaude 選項(透傳):\n -c, --continue 繼續當前目錄最近的對話\n -r, --resume [value] 透過會話 ID 恢復對話,或開啟互動式選擇器\n -p, --print 列印回應後退出(適用於管道)\n -d, --debug [filter] 啟用除錯模式\n -w, --worktree [name] 為此會話建立新的 git worktree\n --model <model> 當前會話使用的模型\n --effort <level> 努力級別 (low, medium, high, max)\n --dangerously-skip-permissions 跳過所有權限檢查\n --allowedTools <tools...> 允許的工具列表\n --disallowedTools <tools...> 禁止的工具列表\n --system-prompt <prompt> 會話系統提示詞\n --append-system-prompt <prompt> 追加系統提示詞\n --mcp-config <configs...> 載入 MCP 伺服器設定\n --permission-mode <mode> 權限模式\n --add-dir <directories...> 額外允許存取的目錄\n --max-budget-usd <amount> API 呼叫最大預算(僅 --print)\n --output-format <format> 輸出格式(僅 --print): text, json, stream-json\n --json-schema <schema> 結構化輸出的 JSON Schema\n --verbose 詳細模式\n -h, --help 顯示說明\n -v, --version 顯示版本\n\n 更多選項請參考: claude --help\n\n範例:\n ccv 啟動互動模式\n ccv -c 繼續上次對話\n ccv -r 恢復對話\n ccv -p \"hello\" 列印模式\n ccv --model opus 指定模型\n ccv --d 跳過權限確認(快捷方式)\n ccv --ad 允許跳過權限(不立即啟用,可透過 Shift+Tab 切換)\n ccv -c --model sonnet --effort max 組合使用\n ccv -SDK Agent SDK 模式(無終端)\n ccv -SDK --d SDK 模式 + 跳過權限\n ccv -logger 安裝/修復 Hook\n ccv --log-dir ~/my-logs 使用自訂日誌目錄\n ccv --no-open 啟動時不開啟瀏覽器\n ccv --user-name \"Alice\" --user-avatar ./avatar.png 自訂使用者身份\n ccv --log-dir /tmp/test --no-open 組合 CCV 選項"
|
|
229
229
|
},
|
|
230
230
|
"cli.cMode.notFound": {
|
|
@@ -2935,6 +2935,66 @@ const i18nData = {
|
|
|
2935
2935
|
"tr": "Hata: --im bir platform kimliği gerektirir (örn. dingtalk)",
|
|
2936
2936
|
"uk": "Помилка: --im потребує id платформи (напр. dingtalk)"
|
|
2937
2937
|
},
|
|
2938
|
+
"cli.pidInvalid": {
|
|
2939
|
+
"zh": "错误: --pid 需要一个非空的实例名(如 --pid=alpha)",
|
|
2940
|
+
"en": "Error: --pid requires a non-empty instance name (e.g. --pid=alpha)",
|
|
2941
|
+
"zh-TW": "錯誤: --pid 需要一個非空的實例名稱(如 --pid=alpha)",
|
|
2942
|
+
"ko": "오류: --pid에는 비어 있지 않은 인스턴스 이름이 필요합니다 (예: --pid=alpha)",
|
|
2943
|
+
"ja": "エラー: --pid には空でないインスタンス名が必要です (例: --pid=alpha)",
|
|
2944
|
+
"de": "Fehler: --pid erfordert einen nicht leeren Instanznamen (z. B. --pid=alpha)",
|
|
2945
|
+
"es": "Error: --pid requiere un nombre de instancia no vacío (p. ej. --pid=alpha)",
|
|
2946
|
+
"fr": "Erreur : --pid nécessite un nom d'instance non vide (p. ex. --pid=alpha)",
|
|
2947
|
+
"it": "Errore: --pid richiede un nome di istanza non vuoto (es. --pid=alpha)",
|
|
2948
|
+
"da": "Fejl: --pid kræver et ikke-tomt instansnavn (f.eks. --pid=alpha)",
|
|
2949
|
+
"pl": "Błąd: --pid wymaga niepustej nazwy instancji (np. --pid=alpha)",
|
|
2950
|
+
"ru": "Ошибка: --pid требует непустого имени экземпляра (например, --pid=alpha)",
|
|
2951
|
+
"ar": "خطأ: --pid يتطلب اسم نسخة غير فارغ (مثل --pid=alpha)",
|
|
2952
|
+
"no": "Feil: --pid krever et ikke-tomt instansnavn (f.eks. --pid=alpha)",
|
|
2953
|
+
"pt-BR": "Erro: --pid requer um nome de instância não vazio (ex.: --pid=alpha)",
|
|
2954
|
+
"th": "ข้อผิดพลาด: --pid ต้องการชื่ออินสแตนซ์ที่ไม่ว่าง (เช่น --pid=alpha)",
|
|
2955
|
+
"tr": "Hata: --pid boş olmayan bir örnek adı gerektirir (örn. --pid=alpha)",
|
|
2956
|
+
"uk": "Помилка: --pid потребує непорожнього імені екземпляра (напр. --pid=alpha)"
|
|
2957
|
+
},
|
|
2958
|
+
"cli.instanceId": {
|
|
2959
|
+
"zh": "实例 PID: {id}",
|
|
2960
|
+
"en": "Instance PID: {id}",
|
|
2961
|
+
"zh-TW": "實例 PID: {id}",
|
|
2962
|
+
"ko": "인스턴스 PID: {id}",
|
|
2963
|
+
"ja": "インスタンス PID: {id}",
|
|
2964
|
+
"de": "Instanz-PID: {id}",
|
|
2965
|
+
"es": "PID de instancia: {id}",
|
|
2966
|
+
"fr": "PID d'instance : {id}",
|
|
2967
|
+
"it": "PID istanza: {id}",
|
|
2968
|
+
"da": "Instans-PID: {id}",
|
|
2969
|
+
"pl": "PID instancji: {id}",
|
|
2970
|
+
"ru": "PID экземпляра: {id}",
|
|
2971
|
+
"ar": "PID النسخة: {id}",
|
|
2972
|
+
"no": "Instans-PID: {id}",
|
|
2973
|
+
"pt-BR": "PID da instância: {id}",
|
|
2974
|
+
"th": "PID อินสแตนซ์: {id}",
|
|
2975
|
+
"tr": "Örnek PID: {id}",
|
|
2976
|
+
"uk": "PID екземпляра: {id}"
|
|
2977
|
+
},
|
|
2978
|
+
"cli.instanceHistory": {
|
|
2979
|
+
"zh": "该项目历史 PID: {ids}",
|
|
2980
|
+
"en": "Past PIDs for this project: {ids}",
|
|
2981
|
+
"zh-TW": "此專案歷史 PID: {ids}",
|
|
2982
|
+
"ko": "이 프로젝트의 이전 PID: {ids}",
|
|
2983
|
+
"ja": "このプロジェクトの過去の PID: {ids}",
|
|
2984
|
+
"de": "Frühere PIDs für dieses Projekt: {ids}",
|
|
2985
|
+
"es": "PIDs anteriores de este proyecto: {ids}",
|
|
2986
|
+
"fr": "PID précédents pour ce projet : {ids}",
|
|
2987
|
+
"it": "PID precedenti per questo progetto: {ids}",
|
|
2988
|
+
"da": "Tidligere PID'er for dette projekt: {ids}",
|
|
2989
|
+
"pl": "Poprzednie PID-y tego projektu: {ids}",
|
|
2990
|
+
"ru": "Прежние PID этого проекта: {ids}",
|
|
2991
|
+
"ar": "معرّفات PID السابقة لهذا المشروع: {ids}",
|
|
2992
|
+
"no": "Tidligere PID-er for dette prosjektet: {ids}",
|
|
2993
|
+
"pt-BR": "PIDs anteriores deste projeto: {ids}",
|
|
2994
|
+
"th": "PID ก่อนหน้าของโปรเจกต์นี้: {ids}",
|
|
2995
|
+
"tr": "Bu projenin önceki PID'leri: {ids}",
|
|
2996
|
+
"uk": "Попередні PID цього проєкту: {ids}"
|
|
2997
|
+
},
|
|
2938
2998
|
"cli.imUnknownPlatform": {
|
|
2939
2999
|
"zh": "错误: 未知的 IM 平台: {id}",
|
|
2940
3000
|
"en": "Error: unknown IM platform: {id}",
|
package/server/interceptor.js
CHANGED
|
@@ -16,7 +16,7 @@ import { homedir } from 'node:os';
|
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
17
|
import { dirname, join, basename } from 'node:path';
|
|
18
18
|
import { LOG_DIR } from '../findcc.js';
|
|
19
|
-
import { assembleStreamMessage, createStreamAssembler, cleanupTempFiles, findRecentLog, isAnthropicApiPath, isMainAgentRequest, rotateLogFile, fingerprintMsg, replaceTopLevelModel } from './lib/interceptor-core.js';
|
|
19
|
+
import { assembleStreamMessage, createStreamAssembler, cleanupTempFiles, findRecentLog, claimUntaggedLog, logFilePrefix, isAnthropicApiPath, isMainAgentRequest, rotateLogFile, fingerprintMsg, replaceTopLevelModel } from './lib/interceptor-core.js';
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
|
|
@@ -195,7 +195,11 @@ function generateNewLogFilePath() {
|
|
|
195
195
|
const projectName = basename(cwd).replace(/[^a-zA-Z0-9_\-\.]/g, '_');
|
|
196
196
|
const dir = join(LOG_DIR, projectName);
|
|
197
197
|
try { mkdirSync(dir, { recursive: true }); } catch { }
|
|
198
|
-
|
|
198
|
+
// `--pid`(CCV_INSTANCE_ID) 实例:文件名前缀 `<pid>__`,让每个实例只读/续自己的日志血脉(多进程隔离)。
|
|
199
|
+
// 内部读 env(与上面内部读 cwd 同理)→ 轮转/workspace 等所有 caller 自动带前缀,无需逐处传参。
|
|
200
|
+
// 前缀走单一来源 logFilePrefix(与 matcher 共用,防漂移)。
|
|
201
|
+
const instanceId = process.env.CCV_INSTANCE_ID || '';
|
|
202
|
+
return { filePath: join(dir, `${logFilePrefix(projectName, instanceId)}${ts}.jsonl`), dir, projectName };
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
// Resume 状态(供 server.js 使用)
|
|
@@ -310,6 +314,10 @@ let _teamName = null;
|
|
|
310
314
|
if (teamIdx !== -1 && teamIdx + 1 < args.length) _teamName = args[teamIdx + 1];
|
|
311
315
|
}
|
|
312
316
|
|
|
317
|
+
// `--pid` 实例 id(cli.js 在 import server 前已设 env,时序安全)。null = 默认模式(不分实例)。
|
|
318
|
+
// 只用于日志文件名的分实例隔离(findRecentLog/cleanupTempFiles/claim);不影响 claude 自己的 -c。
|
|
319
|
+
const INSTANCE_ID = process.env.CCV_INSTANCE_ID || null;
|
|
320
|
+
|
|
313
321
|
// 初始化日志文件路径(异步,支持用户交互)
|
|
314
322
|
// 工作区模式下延迟到选择工作区后再初始化
|
|
315
323
|
let _newLogFile, _logDir, _projectName;
|
|
@@ -323,12 +331,13 @@ if (process.env.CCV_WORKSPACE_MODE === '1') {
|
|
|
323
331
|
try { cwd = process.cwd(); } catch { cwd = homedir(); }
|
|
324
332
|
_projectName = basename(cwd).replace(/[^a-zA-Z0-9_\-\.]/g, '_');
|
|
325
333
|
_logDir = join(LOG_DIR, _projectName);
|
|
326
|
-
|
|
334
|
+
// teammate 子进程继承同一 env(含 CCV_INSTANCE_ID)→ 命中 leader 的 pid 日志;无 env 时为 null = 现状。
|
|
335
|
+
const _leaderLog = findRecentLog(_logDir, _projectName, INSTANCE_ID);
|
|
327
336
|
_newLogFile = _leaderLog || ''; // 没有 leader 日志时不写入
|
|
328
337
|
} else {
|
|
329
338
|
({ filePath: _newLogFile, dir: _logDir, projectName: _projectName } = generateNewLogFilePath());
|
|
330
|
-
//
|
|
331
|
-
cleanupTempFiles(_logDir, _projectName);
|
|
339
|
+
// 启动时清理残留临时文件(按本实例收窄,避免多进程下 rename 掉别的实例正在写的 _temp)
|
|
340
|
+
cleanupTempFiles(_logDir, _projectName, INSTANCE_ID);
|
|
332
341
|
}
|
|
333
342
|
let LOG_FILE = _newLogFile;
|
|
334
343
|
|
|
@@ -344,7 +353,14 @@ const _initPromise = (async () => {
|
|
|
344
353
|
if (!_logDir || !_projectName) return; // 工作区模式下跳过
|
|
345
354
|
if (_isTeammate) return; // Teammate 已在上方同步初始化,跳过 async resume 流程
|
|
346
355
|
try {
|
|
347
|
-
|
|
356
|
+
let recentLog = findRecentLog(_logDir, _projectName, INSTANCE_ID);
|
|
357
|
+
// 首启接管(仅 --pid 实例):本 pid 还没有自己的日志时,可接管项目里最近的【无标签】日志
|
|
358
|
+
// (原子 rename 成 `<pid>__…`),从此并入该 pid 血脉。claim 必须在 build _resumeState 之前,
|
|
359
|
+
// 这样 _resumeState.recentFile 指向 rename 后的新路径(否则会指向已被移走的旧名)。
|
|
360
|
+
if (!recentLog && INSTANCE_ID) {
|
|
361
|
+
const claimed = claimUntaggedLog(_logDir, _projectName, INSTANCE_ID);
|
|
362
|
+
if (claimed) recentLog = claimed;
|
|
363
|
+
}
|
|
348
364
|
if (recentLog) {
|
|
349
365
|
// IM worker:无人值守、无 UI 可应答 resume 交互。直接 continue 最近会话日志(保留记忆、
|
|
350
366
|
// 让记录弹窗读到同一份持续增长的文件),不进入 resume 交互状态(否则会一直写 *_temp.jsonl,
|
|
@@ -374,11 +390,17 @@ export function initForWorkspace(projectPath, { forceNew = false } = {}) {
|
|
|
374
390
|
const dir = join(LOG_DIR, projectName);
|
|
375
391
|
try { mkdirSync(dir, { recursive: true }); } catch {}
|
|
376
392
|
|
|
377
|
-
cleanupTempFiles(dir, projectName);
|
|
393
|
+
cleanupTempFiles(dir, projectName, INSTANCE_ID);
|
|
378
394
|
|
|
379
395
|
// 检查是否有最近的日志文件可以复用(始终复用最新日志)
|
|
380
396
|
// forceNew: Electron multi-tab 模式下强制创建新文件,避免与已有 ccv 实例共享日志
|
|
381
|
-
|
|
397
|
+
let recentLog = !forceNew && findRecentLog(dir, projectName, INSTANCE_ID);
|
|
398
|
+
// 首启接管(与单项目模式 _initPromise 一致):本 pid 在该 workspace 还没有自己的日志时,
|
|
399
|
+
// 接管最近的无标签日志,避免 workspace 模式下 --pid 首启不接管的不一致。forceNew 时显式不接管。
|
|
400
|
+
if (!recentLog && INSTANCE_ID && !forceNew) {
|
|
401
|
+
const claimed = claimUntaggedLog(dir, projectName, INSTANCE_ID);
|
|
402
|
+
if (claimed) recentLog = claimed;
|
|
403
|
+
}
|
|
382
404
|
if (recentLog) {
|
|
383
405
|
_projectName = projectName;
|
|
384
406
|
_logDir = dir;
|
|
@@ -398,7 +420,8 @@ export function initForWorkspace(projectPath, { forceNew = false } = {}) {
|
|
|
398
420
|
+ String(now.getMinutes()).padStart(2, '0')
|
|
399
421
|
+ String(now.getSeconds()).padStart(2, '0');
|
|
400
422
|
|
|
401
|
-
|
|
423
|
+
// 与 generateNewLogFilePath 同款命名(共用 logFilePrefix):`--pid` 实例带 `<pid>__` 前缀。
|
|
424
|
+
const filePath = join(dir, `${logFilePrefix(projectName, INSTANCE_ID)}${ts}.jsonl`);
|
|
402
425
|
|
|
403
426
|
_projectName = projectName;
|
|
404
427
|
_logDir = dir;
|
package/server/lib/auth.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// unit-tested directly (loopback HTTP tests always look local, so they cannot
|
|
10
10
|
// exercise the remote branch — `decideAuth` covers it as a pure function).
|
|
11
11
|
import { randomBytes } from 'node:crypto';
|
|
12
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from 'node:fs';
|
|
13
13
|
import { join, dirname } from 'node:path';
|
|
14
|
+
import { renameSyncWithRetry } from './file-api.js';
|
|
14
15
|
import { LOG_DIR } from '../../findcc.js';
|
|
15
16
|
import { tFor, localeFromAcceptLanguage } from '../i18n.js';
|
|
16
17
|
|
|
@@ -141,10 +142,24 @@ function writePrefs(prefs) {
|
|
|
141
142
|
const p = getPrefsPath();
|
|
142
143
|
const dir = dirname(p);
|
|
143
144
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
145
|
+
// Atomic tmp→rename so a crash mid-write can't truncate the password-bearing file
|
|
146
|
+
// (preferences.json also holds UI prefs + per-project forks). Mirrors prefs-store.js.
|
|
147
|
+
// TODO(prefs-lock): this path does NOT take the prefs-store lock (writePrefs is sync, its
|
|
148
|
+
// callers are sync) — auth writes are rare/admin-initiated, so the residual lost-update
|
|
149
|
+
// window vs a concurrent locked prefs write is accepted; atomicity rules out corruption.
|
|
150
|
+
// Dormant only while every mutatePrefs mutator stays synchronous (no await between its
|
|
151
|
+
// locked read and write); route auth writes through prefs-store.mutatePrefs if that changes.
|
|
152
|
+
const tmp = `${p}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
153
|
+
try {
|
|
154
|
+
writeFileSync(tmp, JSON.stringify(prefs, null, 2), { mode: 0o600 });
|
|
155
|
+
renameSyncWithRetry(tmp, p);
|
|
156
|
+
// writeFileSync's mode only applies on creation; a pre-existing file keeps its old
|
|
157
|
+
// perms. Re-assert 0600 — preferences.json now carries the (base64) password.
|
|
158
|
+
try { chmodSync(p, 0o600); } catch { /* best-effort; non-POSIX or race */ }
|
|
159
|
+
} catch (err) {
|
|
160
|
+
try { unlinkSync(tmp); } catch {}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
/**
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Passive registry of instance ids (`--pid`) used per project — a memory aid only.
|
|
2
|
+
//
|
|
3
|
+
// On startup with `--pid X`, the server appends X to `{projectDir}/.instances.json` so the
|
|
4
|
+
// CLI banner can remind the user which ids they've used ("history: alpha, beta") next time.
|
|
5
|
+
// This is NOT a liveness / auto-numbering registry: entries are never probed or auto-assigned,
|
|
6
|
+
// just remembered (deduped, most-recent-last, capped). `logDir` is the per-project dir;
|
|
7
|
+
// '' (no active project) → record no-ops / list returns [].
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
11
|
+
import { renameSyncWithRetry } from './file-api.js';
|
|
12
|
+
import { withFileLockAsync } from './async-file-lock.js';
|
|
13
|
+
import { sanitizeInstanceId } from './session-pin-store.js';
|
|
14
|
+
|
|
15
|
+
const MAX_INSTANCES = 50; // cap the list; oldest entries drop off (memory aid, not an audit log)
|
|
16
|
+
|
|
17
|
+
function instancesFile(logDir) { return join(logDir, '.instances.json'); }
|
|
18
|
+
|
|
19
|
+
function readListRaw(logDir) {
|
|
20
|
+
try {
|
|
21
|
+
const file = instancesFile(logDir);
|
|
22
|
+
if (!existsSync(file)) return [];
|
|
23
|
+
const arr = JSON.parse(readFileSync(file, 'utf-8'));
|
|
24
|
+
return Array.isArray(arr) ? arr.filter((x) => typeof x === 'string' && x) : [];
|
|
25
|
+
} catch { return []; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Previously-used instance ids for this project, oldest → newest. No project → []. */
|
|
29
|
+
export function listInstances(logDir) {
|
|
30
|
+
if (!logDir) return [];
|
|
31
|
+
return readListRaw(logDir);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record an instance id: dedupe (move-to-end so "most recent" wins), cap at MAX_INSTANCES.
|
|
36
|
+
* Cross-process safe via the shared async file lock + atomic tmp→rename. No project /
|
|
37
|
+
* empty id → no-op.
|
|
38
|
+
*/
|
|
39
|
+
export async function recordInstance(logDir, instanceId) {
|
|
40
|
+
if (!logDir) return;
|
|
41
|
+
const id = sanitizeInstanceId(instanceId);
|
|
42
|
+
if (!id) return;
|
|
43
|
+
await withFileLockAsync(join(logDir, '.instances.lock'), () => {
|
|
44
|
+
const list = readListRaw(logDir).filter((x) => x !== id);
|
|
45
|
+
list.push(id);
|
|
46
|
+
while (list.length > MAX_INSTANCES) list.shift();
|
|
47
|
+
const file = instancesFile(logDir);
|
|
48
|
+
const tmp = `${file}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(tmp, JSON.stringify(list));
|
|
51
|
+
renameSyncWithRetry(tmp, file);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
try { unlinkSync(tmp); } catch {}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}, { ensureDir: logDir });
|
|
57
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { renameSyncWithRetry } from './file-api.js';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, basename } from 'node:path';
|
|
4
4
|
|
|
5
5
|
const SUBAGENT_SYSTEM_RE = /(?:command execution|file search|planning) specialist|general-purpose agent|security monitor|performing a web search/i;
|
|
6
6
|
|
|
@@ -251,12 +251,33 @@ export function createStreamAssembler() {
|
|
|
251
251
|
};
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
// 日志文件名前缀【单一来源】:writer(generateNewLogFilePath / initForWorkspace)与 matcher 共用,
|
|
255
|
+
// 防止两侧命名漂移。默认 `<project>_`;`--pid` 实例 `<pid>__<project>_`。完整文件名 = `${prefix}${ts}.jsonl`。
|
|
256
|
+
export function logFilePrefix(projectName, instanceId) {
|
|
257
|
+
return instanceId ? `${instanceId}__${projectName}_` : `${projectName}_`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 日志文件名归属判定(实例隔离)。`--pid` 实例文件名为 `<pid>__<project>_<ts>…`,无 pid 为 `<project>_<ts>…`。
|
|
261
|
+
// - instanceId 非空:必须精确以 `<pid>__<project>_` 开头(只认本实例)。
|
|
262
|
+
// - instanceId 空(默认):以 `<project>_` 开头【且】不含 pid 分隔特征 `__<project>_`。后一条堵住前缀碰撞——
|
|
263
|
+
// 若用户把 --pid 取成项目名或 `<项目>_…`(如项目 proj 用 --pid=proj → `proj__proj_…` 也会 startsWith `proj_`),
|
|
264
|
+
// 仅靠前缀无法排除;而任何 pid 文件按构造必含 `__<project>_`,无标签文件绝不含(即便项目名自身带 `__`),据此精确排除。
|
|
265
|
+
export function logFileMatcher(projectName, instanceId) {
|
|
266
|
+
const p = logFilePrefix(projectName, instanceId);
|
|
267
|
+
if (instanceId) return (f) => f.startsWith(p);
|
|
268
|
+
const pidMark = `__${projectName}_`;
|
|
269
|
+
return (f) => f.startsWith(p) && !f.includes(pidMark);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// instanceId 为可选第 3 参(默认 null = 现状):非空时只在本实例自己的日志里找(多进程隔离),
|
|
273
|
+
// 现有 2 参调用方与单测零改动。
|
|
274
|
+
export function findRecentLog(dir, projectName, instanceId = null) {
|
|
255
275
|
try {
|
|
276
|
+
const owns = logFileMatcher(projectName, instanceId);
|
|
256
277
|
const files = readdirSync(dir)
|
|
257
278
|
// 排除 *_temp.jsonl:临时文件是未完成的写入态(resume 流程中途产物),
|
|
258
279
|
// 不应被当作"最近完整日志"(否则 _temp 因 sort 排在正式文件之后会被误选)。
|
|
259
|
-
.filter(f => f
|
|
280
|
+
.filter(f => owns(f) && f.endsWith('.jsonl') && !f.endsWith('_temp.jsonl'))
|
|
260
281
|
.sort()
|
|
261
282
|
.reverse();
|
|
262
283
|
if (files.length === 0) return null;
|
|
@@ -265,10 +286,37 @@ export function findRecentLog(dir, projectName) {
|
|
|
265
286
|
return null;
|
|
266
287
|
}
|
|
267
288
|
|
|
268
|
-
|
|
289
|
+
// 首启接管(仅 --pid 实例):本 pid 还没有自己的日志时,把项目里最近的【无标签】日志原子 rename 成
|
|
290
|
+
// `<pid>__<project>_<ts>.jsonl`,并入该 pid 血脉(move 而非 copy:不双计入统计)。返回接管后的新路径,或 null。
|
|
291
|
+
//
|
|
292
|
+
// 这是 best-effort 便利特性,不是核心隔离(核心隔离=各 pid 读自己的日志,不依赖接管)。为不误抢一个仍存活的
|
|
293
|
+
// 无 pid 实例(评审标记的 live-writer / 卡在 resume 的窄边角),双重守卫:① 项目里若存在任何无标签 `*_temp.jsonl`
|
|
294
|
+
// (某个无 pid 实例正卡在 resume / 在写)→ 放弃;② 无标签日志 mtime 必须早于 freshnessMs(默认 5min)。仍有理论窄缝
|
|
295
|
+
// (进程挂起 >5min 且无 temp),属可接受的便利取舍。并发下原子 rename 只一个赢、输者 throw→吞→null(回退全新)。
|
|
296
|
+
export function claimUntaggedLog(dir, projectName, instanceId, { freshnessMs = 300000 } = {}) {
|
|
297
|
+
if (!instanceId) return null;
|
|
298
|
+
try {
|
|
299
|
+
const untagged = findRecentLog(dir, projectName, null); // 最近无标签日志(已排除 pid 文件 + temp)
|
|
300
|
+
if (!untagged) return null;
|
|
301
|
+
// ① 存在无标签 temp ⇒ 有无 pid 实例正活动/卡在 resume → 不接管。
|
|
302
|
+
const owns = logFileMatcher(projectName, null);
|
|
303
|
+
const hasUntaggedTemp = readdirSync(dir).some(f => owns(f) && f.endsWith('_temp.jsonl'));
|
|
304
|
+
if (hasUntaggedTemp) return null;
|
|
305
|
+
let st;
|
|
306
|
+
try { st = statSync(untagged); } catch { return null; }
|
|
307
|
+
if (Date.now() - st.mtimeMs < freshnessMs) return null; // ② 可能是活动写者 → 不碰
|
|
308
|
+
const claimed = join(dir, `${instanceId}__${basename(untagged)}`);
|
|
309
|
+
if (existsSync(claimed)) return null; // 极端撞名 → 放弃
|
|
310
|
+
renameSyncWithRetry(untagged, claimed);
|
|
311
|
+
return claimed;
|
|
312
|
+
} catch { return null; }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function cleanupTempFiles(dir, projectName, instanceId = null) {
|
|
269
316
|
try {
|
|
317
|
+
const owns = logFileMatcher(projectName, instanceId);
|
|
270
318
|
const tempFiles = readdirSync(dir)
|
|
271
|
-
.filter(f => f
|
|
319
|
+
.filter(f => owns(f) && f.endsWith('_temp.jsonl'));
|
|
272
320
|
for (const f of tempFiles) {
|
|
273
321
|
try {
|
|
274
322
|
const tempPath = join(dir, f);
|
|
@@ -6,6 +6,7 @@ import { join, sep } from 'node:path';
|
|
|
6
6
|
import { reconstructEntries } from './delta-reconstructor.js';
|
|
7
7
|
import { streamReconstructedEntriesAsync } from './log-stream.js';
|
|
8
8
|
import { archiveJsonl, resolveJsonlPath } from './jsonl-archive.js';
|
|
9
|
+
import { logFileMatcher } from './interceptor-core.js';
|
|
9
10
|
|
|
10
11
|
export function validateLogPath(logDir, file) {
|
|
11
12
|
const filePath = join(logDir, file);
|
|
@@ -28,7 +29,20 @@ function isLogFileName(name) {
|
|
|
28
29
|
return name.endsWith('.jsonl') || name.endsWith('.jsonl.zip');
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
// 解析日志文件名里的时间戳 `YYYYMMDD_HHMMSS`(带不带 `<pid>__` 前缀、归档与否都适用)。
|
|
33
|
+
// 用于「按时间排序 / 判最新」——文件名整串排序会把 `<pid>__` 前缀('1' < 'c')的最新文件排到最底,
|
|
34
|
+
// 必须按时间戳排。无法解析时返回 ''(排到最后)。listLocalLogs 与 archiveLogFiles 共用,防漂移。
|
|
35
|
+
function parseLogTs(name) {
|
|
36
|
+
const m = name.match(/_(\d{8}_\d{6})\.jsonl(\.zip)?$/);
|
|
37
|
+
return m ? m[1] : '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// instanceId / showAll 为可选项(默认 null / false = 现状的反面:硬隔离):
|
|
41
|
+
// - instanceId 非空:只列本实例 `<pid>__<project>_…` 的日志;
|
|
42
|
+
// - instanceId 空:只列无标签 `<project>_…` 日志(排除任何 `<pid>__` 文件);
|
|
43
|
+
// - showAll=true:越过上面的归属过滤,列出目录下全部日志(顶部「显示全部」开关用)。
|
|
44
|
+
// 复用 interceptor-core 的 logFileMatcher(与写入端共用,防命名漂移)。第 3 参可选 → 旧 2 参调用零改动。
|
|
45
|
+
export async function listLocalLogs(logDir, currentProjectName, { instanceId = null, showAll = false } = {}) {
|
|
32
46
|
const grouped = {};
|
|
33
47
|
if (!existsSync(logDir)) return { ...grouped, _currentProject: currentProjectName || '' };
|
|
34
48
|
|
|
@@ -37,30 +51,47 @@ export async function listLocalLogs(logDir, currentProjectName) {
|
|
|
37
51
|
if (!entry.isDirectory()) continue;
|
|
38
52
|
const project = entry.name;
|
|
39
53
|
const projectDir = join(logDir, project);
|
|
40
|
-
|
|
41
|
-
.filter(isLogFileName)
|
|
42
|
-
.sort()
|
|
43
|
-
.reverse();
|
|
44
|
-
let statsFiles = null;
|
|
54
|
+
// 单个项目目录读失败(权限 / 被并发删除 / EMFILE 等)只跳过该项目,不拖垮整个列表。
|
|
45
55
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
const owns = logFileMatcher(project, instanceId);
|
|
57
|
+
const files = (await readdir(projectDir)).filter(isLogFileName);
|
|
58
|
+
let statsFiles = null;
|
|
59
|
+
try {
|
|
60
|
+
const statsFile = join(projectDir, `${project}.json`);
|
|
61
|
+
if (existsSync(statsFile)) {
|
|
62
|
+
statsFiles = JSON.parse(await readFile(statsFile, 'utf-8')).files;
|
|
63
|
+
}
|
|
64
|
+
} catch { }
|
|
65
|
+
for (const f of files) {
|
|
66
|
+
if (!showAll && !owns(f)) continue;
|
|
67
|
+
const match = f.match(/^(.+?)_(\d{8}_\d{6})\.jsonl(\.zip)?$/);
|
|
68
|
+
if (!match) continue;
|
|
69
|
+
const ts = match[2];
|
|
70
|
+
const archived = !!match[3];
|
|
71
|
+
// 从前缀解析归属实例 id 用于行内 badge:`<pid>__<project>` → pid;`<project>` → 无(null)。
|
|
72
|
+
// 项目名自身可含下划线,故用 endsWith('__'+project) 精确剥离,而非按 '__' split。
|
|
73
|
+
const g1 = match[1];
|
|
74
|
+
const fileInstanceId = (g1 !== project && g1.endsWith('__' + project))
|
|
75
|
+
? g1.slice(0, -(project.length + 2))
|
|
76
|
+
: null;
|
|
77
|
+
const filePath = join(projectDir, f);
|
|
78
|
+
let size;
|
|
79
|
+
try { size = (await stat(filePath)).size; } catch { continue; }
|
|
80
|
+
if (size === 0) continue;
|
|
81
|
+
const stats = statsFiles?.[f] || (archived ? statsFiles?.[f.slice(0, -4)] : null);
|
|
82
|
+
const turns = stats?.summary?.sessionCount || 0;
|
|
83
|
+
if (!grouped[project]) grouped[project] = [];
|
|
84
|
+
grouped[project].push({ file: `${project}/${f}`, timestamp: ts, size, turns, preview: stats?.preview || [], archived, instanceId: fileInstanceId });
|
|
49
85
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (size === 0) continue;
|
|
60
|
-
const stats = statsFiles?.[f] || (archived ? statsFiles?.[f.slice(0, -4)] : null);
|
|
61
|
-
const turns = stats?.summary?.sessionCount || 0;
|
|
62
|
-
if (!grouped[project]) grouped[project] = [];
|
|
63
|
-
grouped[project].push({ file: `${project}/${f}`, timestamp: ts, size, turns, preview: stats?.preview || [], archived });
|
|
86
|
+
// 按时间戳降序(文件名降序兜底,处理同秒 tie,保证确定性)。
|
|
87
|
+
// 不能再依赖文件名整串排序:`<pid>__` 前缀会把最新日志排到最底。
|
|
88
|
+
if (grouped[project]) {
|
|
89
|
+
grouped[project].sort((a, b) =>
|
|
90
|
+
b.timestamp.localeCompare(a.timestamp) || b.file.localeCompare(a.file));
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(`[CC Viewer] listLocalLogs: 跳过无法读取的项目 ${project}:`, err?.message || err);
|
|
94
|
+
continue;
|
|
64
95
|
}
|
|
65
96
|
}
|
|
66
97
|
return { ...grouped, _currentProject: currentProjectName || '' };
|
|
@@ -242,10 +273,11 @@ export function archiveLogFiles(logDir, files) {
|
|
|
242
273
|
const projectDir = join(logDir, project);
|
|
243
274
|
let latest = null;
|
|
244
275
|
try {
|
|
276
|
+
// 「最新文件不允许归档」——按时间戳判最新(与 listLocalLogs 同口径),
|
|
277
|
+
// 否则文件名整串排序会把 `<pid>__` 前缀的最新活跃日志误判成非最新而放行归档。
|
|
245
278
|
const projectEntries = readdirSync(projectDir)
|
|
246
279
|
.filter(isLogFileName)
|
|
247
|
-
.sort()
|
|
248
|
-
.reverse();
|
|
280
|
+
.sort((a, b) => parseLogTs(b).localeCompare(parseLogTs(a)) || b.localeCompare(a));
|
|
249
281
|
latest = projectEntries[0] || null;
|
|
250
282
|
} catch {}
|
|
251
283
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Single correct writer for {LOG_DIR}/preferences.json.
|
|
2
|
+
//
|
|
3
|
+
// preferences.json holds UI prefs AND the (base64) LAN password (auth) AND per-project
|
|
4
|
+
// forks (prefsByProject). Multiple writers touch it — the global POST /api/preferences,
|
|
5
|
+
// lib/auth.js, and the new project-prefs routes — so every write here goes through ONE
|
|
6
|
+
// async file lock + atomic tmp→rename, mirroring server/lib/ask-store.js. Same-process
|
|
7
|
+
// callers serialize via withFileLockAsync's per-lockPath Promise chain; cross-process
|
|
8
|
+
// callers mutex on the lock file. This prevents a concurrent writer from clobbering the
|
|
9
|
+
// password-bearing file or losing a fork update.
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from 'node:fs';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
13
|
+
import { renameSyncWithRetry } from './file-api.js';
|
|
14
|
+
import { withFileLockAsync } from './async-file-lock.js';
|
|
15
|
+
import { mergeApprovalModalPrefs } from './approval-modal-prefs.js';
|
|
16
|
+
import { reconcileVoicePackPrefs } from './voice-pack-manager.js';
|
|
17
|
+
import { LOG_DIR } from '../../findcc.js';
|
|
18
|
+
|
|
19
|
+
// Path is computed fresh each call: LOG_DIR is a live binding (setLogDir) and tests
|
|
20
|
+
// redirect it via CCV_LOG_DIR before importing. Matches lib/auth.js getPrefsPath().
|
|
21
|
+
// In production every caller resolves to this same canonical path; the optional `file`
|
|
22
|
+
// override on the helpers below exists only so the preferences route can forward its
|
|
23
|
+
// deps.getPrefsFile() seam (used by branch tests) and stay symmetric with the GET read.
|
|
24
|
+
export function getPrefsFile() { return join(LOG_DIR, 'preferences.json'); }
|
|
25
|
+
// Lock lives next to the file so every writer of the SAME preferences.json shares one lock
|
|
26
|
+
// (global POST, fork ops, …). In prod all paths are canonical ⇒ one lock ⇒ serialized.
|
|
27
|
+
function getPrefsLock(file) { return join(dirname(file), 'preferences.lock'); }
|
|
28
|
+
|
|
29
|
+
/** Read the raw on-disk prefs object (no stripping, no virtual defaults). {} on miss/corrupt. */
|
|
30
|
+
export function readPrefsRaw(file = getPrefsFile()) {
|
|
31
|
+
try {
|
|
32
|
+
if (!existsSync(file)) return {};
|
|
33
|
+
const obj = JSON.parse(readFileSync(file, 'utf-8'));
|
|
34
|
+
return obj && typeof obj === 'object' ? obj : {};
|
|
35
|
+
} catch { return {}; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Atomic write (tmp + rename) with 0600 — the file may carry the base64 password. */
|
|
39
|
+
function writePrefsAtomic(prefs, file) {
|
|
40
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
41
|
+
const tmp = `${file}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
42
|
+
try {
|
|
43
|
+
writeFileSync(tmp, JSON.stringify(prefs, null, 2), { mode: 0o600 });
|
|
44
|
+
renameSyncWithRetry(tmp, file);
|
|
45
|
+
// writeFileSync's mode only applies on creation; re-assert 0600 on a pre-existing file.
|
|
46
|
+
try { chmodSync(file, 0o600); } catch { /* best-effort; non-POSIX or race */ }
|
|
47
|
+
} catch (err) {
|
|
48
|
+
try { unlinkSync(tmp); } catch {}
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Locked read-modify-write. Reads the raw prefs inside the lock, runs mutator(prefs)
|
|
55
|
+
* (mutate in place; may be async), atomically writes, and returns the mutator's return
|
|
56
|
+
* value when defined, else the mutated prefs object. `file` defaults to the canonical path.
|
|
57
|
+
*/
|
|
58
|
+
export async function mutatePrefs(mutator, file = getPrefsFile()) {
|
|
59
|
+
return withFileLockAsync(getPrefsLock(file), async () => {
|
|
60
|
+
const prefs = readPrefsRaw(file);
|
|
61
|
+
const result = await mutator(prefs);
|
|
62
|
+
writePrefsAtomic(prefs, file);
|
|
63
|
+
return result !== undefined ? result : prefs;
|
|
64
|
+
}, { ensureDir: dirname(file) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply a preferences patch onto `target` IN PLACE — the single-sourced merge used by
|
|
69
|
+
* both the global POST and per-project fork writes so the two paths can't drift.
|
|
70
|
+
* Scalars shallow-merge; approvalModal goes through mergeApprovalModalPrefs (with voice
|
|
71
|
+
* pack reconcile when a logDir is given).
|
|
72
|
+
*/
|
|
73
|
+
export function applyPrefsPatch(target, patch, { logDir = null } = {}) {
|
|
74
|
+
if (!patch || typeof patch !== 'object') return target;
|
|
75
|
+
const { approvalModal: incAM, ...rest } = patch;
|
|
76
|
+
Object.assign(target, rest);
|
|
77
|
+
if (incAM && typeof incAM === 'object') {
|
|
78
|
+
target.approvalModal = mergeApprovalModalPrefs(target.approvalModal, incAM, {
|
|
79
|
+
reconcile: logDir ? (vp) => reconcileVoicePackPrefs(logDir, vp) : null,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return target;
|
|
83
|
+
}
|