zen-gitsync 2.18.3 → 2.18.4
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/README.md +6 -6
- package/package.json +3 -2
- package/src/cli/ai/modelSetup.js +120 -7
- package/src/cli/ai/modelSetup.test.js +157 -0
- package/src/ui/public/assets/{AgentView-juB5wmpE.js → AgentView-0eP_Qb7Z.js} +1 -1
- package/src/ui/public/assets/{AppVersionBadge-BDi3TXTG.js → AppVersionBadge-sYoxE7hr.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-B8hW4D64.js → BranchSelector-BJyAmmrt.js} +1 -1
- package/src/ui/public/assets/{CommitForm-epNRsZc3.js → CommitForm-DY6P1_B2.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-CTbNV8Wg.js → CommonDialog-9td95UkD.js} +1 -1
- package/src/ui/public/assets/{CopySessionButton-DEFGS3YY.js → CopySessionButton-De46dtzF.js} +1 -1
- package/src/ui/public/assets/EditorView-B2uGxtRE.js +1 -0
- package/src/ui/public/assets/{EditorView-Cf9u2vkM.css → EditorView-C4eN80vi.css} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-BEip7kmz.js → FlowExecutionViewer-BhgFKaIR.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-Bsszwf6-.js → FlowOrchestrationWorkspace-DQpWyRai.js} +1 -1
- package/src/ui/public/assets/{LogList-EfGFN896.js → LogList-uk1cDDEM.js} +1 -1
- package/src/ui/public/assets/{MindmapView-BM81SZBC.js → MindmapView-Cjk4g62c.js} +1 -1
- package/src/ui/public/assets/{MonitorView-D6yTQA3g.js → MonitorView-BzjrUh7I.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-DFgM7Pgz.js → ProjectStartupButton-C0nPW6ku.js} +1 -1
- package/src/ui/public/assets/{RecentDirectoriesChat-CeAb-DSk.js → RecentDirectoriesChat-DN7CtUV6.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-C7pqRpzl.js → RemoteManagerDialog-5JJITbBA.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-BZcV9S8Z.js → RemoteRepoCard-BAksLs0D.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-Bo7xTfCi.js → SourceMapView-IGoZ_15F.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-_bACg8fN.js → SvgIcon-BPexLObf.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-Cfzi_G75.js → UserInputNode-Bi7sIefa.js} +1 -1
- package/src/ui/public/assets/{WorkbenchView-Ifx4f8MG.js → WorkbenchView-CMsyIBYe.js} +2 -2
- package/src/ui/public/assets/{WorkbenchView-5tjrtuRX.css → WorkbenchView-PaqqcDj9.css} +1 -1
- package/src/ui/public/assets/{_plugin-vue_export-helper-BXpA-5RR.js → _plugin-vue_export-helper-BO6qa5C3.js} +5 -5
- package/src/ui/public/assets/{agentConversations-vfZW1S6H.js → agentConversations-mDAcOgXq.js} +1 -1
- package/src/ui/public/assets/{configStore-BpKN8VOg.js → configStore-D_CFFOs9.js} +1 -1
- package/src/ui/public/assets/{dagre-56EqSb7a.js → dagre-Cnlh8k5K.js} +3 -3
- package/src/ui/public/assets/{element-plus-DAeQkmsV.js → element-plus-CNJIhPSG.js} +1 -1
- package/src/ui/public/assets/{flow-mindmap-DSMXo-RP.js → flow-mindmap-DdKeBM_G.js} +1 -1
- package/src/ui/public/assets/index-BsHc5Rxd.js +57 -0
- package/src/ui/public/assets/index-CUvENjYd.css +1 -0
- package/src/ui/public/assets/{monaco-iQW4tGZA.js → monaco-Cs1I4pfm.js} +1 -1
- package/src/ui/public/assets/{office-docx-CX5czsEV.js → office-docx-DmRJb9rE.js} +1 -1
- package/src/ui/public/assets/{office-excel-D4925wfu.js → office-excel-DavLx3AP.js} +1 -1
- package/src/ui/public/assets/{office-pptx-92XfuytY.js → office-pptx-DYg4i8gh.js} +1 -1
- package/src/ui/public/assets/{vendor-DpIR4Sxg.css → vendor-B3_-csNx.css} +1 -1
- package/src/ui/public/assets/{vendor-ytAMhfHM.js → vendor-Dtqsc1If.js} +4 -4
- package/src/ui/public/assets/{vue-flow-COq_ygQX.js → vue-flow-3t3hqiSq.js} +1 -1
- package/src/ui/public/index.html +13 -13
- package/src/ui/public/assets/EditorView-BFAjQLzT.js +0 -1
- package/src/ui/public/assets/index-DDofdm66.js +0 -57
- package/src/ui/public/assets/index-DnNE9KX8.css +0 -1
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ npm install -g zen-gitsync
|
|
|
61
61
|
- **Project startup** — Auto-run commands or workflows when a project opens
|
|
62
62
|
- **Built-in code editor** — Monaco-based file editor with Markdown preview
|
|
63
63
|
- **Workbench** — a multi-project board with a kanban view and a master-agent dispatch console; task-driven agent execution (Claude Code or OpenCode) with prompt presets, isolated per-task processes, live streaming output, AI-generated presets and task-level attachments
|
|
64
|
-
- **Repository cloning** — clone any GitHub / Gitee repository into a folder straight from the repo browser, with an *Already cloned* badge (and its local path) backed by a whole-disk local-repository scan
|
|
64
|
+
- **Repository cloning** — clone any GitHub / Gitee repository into a folder straight from the repo browser, with an *Already cloned* badge (and its local path(s), including the count when a repo lives in several folders) backed by a whole-disk local-repository scan
|
|
65
65
|
- **Skill / MCP marketplace** — install skills and MCP servers from the Agent view into the current project or the `g ai` agent
|
|
66
66
|
- **Reset to remote** — One-click `git reset --hard origin/<branch>` from the Git panel (auto-refreshes branch info first to avoid wrong-target resets)
|
|
67
67
|
- **AI commit message** — Generate commit message from staged diff automatically
|
|
@@ -187,7 +187,7 @@ In either mode, click **AI Generate** to fill in the fields automatically based
|
|
|
187
187
|
- **No refetch on tab switch** — the list is cached per account, so coming back to the tab paints instantly instead of shelling out to `gh repo list` again; once the cache is a minute old it paints from cache first and refreshes quietly in the background, while **刷新** always pulls for real
|
|
188
188
|
- **Informative cards** — repository name, description and privacy / fork / language / star badges, plus a third line with last-push date, fork count, non-`main` default branch and license (each omitted when there is nothing to say)
|
|
189
189
|
- **Clone straight to a folder** — a repository that is not on your disk yet offers **Clone to folder…**: pick a directory and the clone runs over SSH (`git@github.com:owner/repo.git`), with an `https://` URL normalised first so it never stalls on a Git Credential Manager prompt
|
|
190
|
-
- **"Already cloned" badge** — the server keeps a whole-disk index of local Git repositories (built in the background and refreshable on demand), so a card for a repo you already have shows its local path instead of offering another clone
|
|
190
|
+
- **"Already cloned" badge** — the server keeps a whole-disk index of local Git repositories (built in the background and refreshable on demand), so a card for a repo you already have shows its local path instead of offering another clone; cloned the same repo into several folders and the badge calls out the count (`Cloned ×2`), the tooltip lists every path, and Ctrl+click opens a menu to pick which copy to launch `g ui` in
|
|
191
191
|
- **One click to open or copy** — clicking a card opens the repository page in your browser; the actions that appear on hover copy the URL or open it
|
|
192
192
|
|
|
193
193
|
---
|
|
@@ -406,7 +406,7 @@ A dedicated view for running coding agents across one or many projects. Every ta
|
|
|
406
406
|
| Dispatch default prompts | Give every dispatch a standing prompt: one **global** entry that applies to all projects, plus one **per project** that is appended after it whenever you dispatch to that project (it supplements the global one rather than replacing it). Both are edited from the gear button in the console's composer; the combined text is prepended to the instruction, a "Default prompt (global + this project)" checkbox appears next to **Run now** so a single dispatch can opt out, and the instruction log records which level was attached. The prompt is copied onto the task at dispatch time, so editing the setting later never rewrites tasks that already exist; it lands in the task's own prompt field, where you can still edit it per task |
|
|
407
407
|
| Open-with menu per project | Hovering a project row in the board's project list reveals two buttons: **Open folder** (straight to your file manager) and **Open with**, a menu holding file manager / terminal / `g ui` in a new tab plus every editor and AI tool (VS Code, Codex, OpenCode, Kimi Code, ZCode, DeepSeek Harness, and Claude Code in default or fully-approved mode). Tools that are not installed are dimmed and labelled "Not installed" — clicking one opens the very same install guide the top bar uses. Every action applies to that row's project only; the board's selection is never touched |
|
|
408
408
|
| Remove a dead project | A row whose folder no longer exists still shows up — the list is the union of recent directories and the paths your tasks remember — so that row gets a single red **Remove from list** button instead of the open actions (opening a missing folder can only fail). The confirm dialog spells it out: **no tasks are deleted**, and the toast repeats how many were kept. The entry disappears from the list while every task, job and history record stays put; if you ever clone the folder back, the row returns on its own |
|
|
409
|
-
| Task editor overlay | Clicking a board card opens the editor as an overlay: a flat sidebar holding the task list and prompt presets, then the task header, preset selector, executor split-button, the copy-execution / execution-log / clear-execution actions, and a chat-style execution body. The description collapses into a one-line "Task description (optional)" summary until clicked, showing a "Filled" badge and the attachment count once there is content. Opening the overlay lands the conversation on its **newest** turn rather than its first: the flow keeps re-pinning to the bottom while markdown highlighting and tool-call folding are still growing it after mount, and lets go as soon as you scroll yourself. The thin bar on top carries the back-to-board button
|
|
409
|
+
| Task editor overlay | Clicking a board card opens the editor as an overlay: a flat sidebar holding the task list and prompt presets, then the task header, preset selector, executor split-button, the copy-execution / execution-log / clear-execution actions, and a chat-style execution body. The description collapses into a one-line "Task description (optional)" summary until clicked, showing a "Filled" badge and the attachment count once there is content. Opening the overlay lands the conversation on its **newest** turn rather than its first: the flow keeps re-pinning to the bottom while markdown highlighting and tool-call folding are still growing it after mount, and lets go as soon as you scroll yourself. The thin bar on top carries the back-to-board button, the current project, then a **task-status badge** (running / done / failed / to-do, in the same role colors as the board column headers, its dot pulsing while it runs; hover for the "AI marked done" / "marked done manually" reasoning — before this the bar showed only time and duration, so a running task could still wear last turn's "1 hour ago · took 13 min" and look finished), and — just left of the "Task execution" label at its right end — the task's **relative time and how long it took** (a "took x" line once it has run, a live "running for x" while it is, and only the time — no placeholder — if it never ran), word for word the same two values the board card shows. When the window narrows, this block and the status badge yield first (the badge is dropped entirely at roughly phone width rather than squashing the project name): the back button, project and execution-path chips never give up a single pixel |
|
|
410
410
|
| Copy execution content | **Copy execution content** in the task header puts the whole task — every turn, not just whatever is on screen — on the clipboard as plain Markdown: a header line for the task and its project, then one section per turn (`## Round N`, with the executor, status and start time on the line under it) holding the user's prompt, the agent's thinking, its tool calls (arguments and results in fenced blocks) and the model's output. It is assembled from the run records rather than scraped from the DOM, so what you get never depends on what happens to be selected. The user side goes through the same trimming the chat bubbles use, so what you copy is what you typed — the injected environment / memory blocks and the attachment list stay behind in `job.prompt`. Turns with nothing in them are skipped without renumbering the ones after them, and a task that has never run answers with a "nothing to copy" toast instead of putting an empty string on the clipboard |
|
|
411
411
|
| Executor choice | Run each task with **Claude Code**, **OpenCode** or **Codex**. The global default is set in **Settings → General → Task executor** (`config.taskExecutor`); the split-button next to the run button switches it for the next run and remembers that pick in the browser. A continued conversation always stays on the executor that started it — Claude's `--resume`, OpenCode's `--session` and Codex's thread id are not interchangeable |
|
|
412
412
|
| Attachments | Any number of files per task (image / PDF / text / Markdown / CSV / JSON / log, ≤ 20 MB each); images over 3.5 MB are re-encoded / downscaled in the browser before upload so 4K screenshots still fit what the model and the reader can take. Their absolute paths are appended to the prompt so the agent reads them directly. Right-click an image attachment to copy it to the system clipboard (`image/png` / `jpeg` / `webp` / `gif`). The board's create-task dialog takes them too, before the task exists: files are staged server-side (`workbench-images/_dispatch/`) and claimed into the task's own folder the moment you hit Create, the create button stays disabled while an upload is in flight, and closing the dialog without creating deletes them instead of leaving screenshots behind. File names travel percent-encoded, so a name written in Chinese uploads like any other |
|
|
@@ -762,7 +762,7 @@ npm install -g zen-gitsync
|
|
|
762
762
|
- **项目启动** — 打开项目时自动运行命令或工作流
|
|
763
763
|
- **内置代码编辑器** — 基于 Monaco 的文件编辑器,支持 Markdown 预览
|
|
764
764
|
- **工作台** — 多项目看板 + 主 Agent 派发控制台;任务驱动的智能体执行(Claude Code 或 OpenCode),支持提示词预置、任务级附件、独立进程、实时流式回传与 AI 生成预置提示词
|
|
765
|
-
- **仓库克隆** — 在仓库浏览器里把任意 GitHub / Gitee
|
|
765
|
+
- **仓库克隆** — 在仓库浏览器里把任意 GitHub / Gitee 仓库直接克隆到指定文件夹,卡片带「已克隆」徽标与本地路径(同一仓库克隆到多处时会显示条数,由全盘本地仓库扫描得出)
|
|
766
766
|
- **Skill / MCP 广场** — 在智能体页把 Skill 与 MCP 服务安装到当前项目或 `g ai` 智能体
|
|
767
767
|
- **重置到远程** — 在 Git 面板一键执行 `git reset --hard origin/<branch>`(点击前会先自动刷新分支信息,避免重置到陈旧分支)
|
|
768
768
|
- **AI 生成提交信息** — 基于 staged diff 自动生成提交消息
|
|
@@ -919,7 +919,7 @@ $ ZEN_ALLOWED_ORIGINS="https://zen.example.com,http://10.0.0.5:8080" g ui
|
|
|
919
919
|
- **切 Tab 不重拉** — 列表按账号各缓存一份,切回来直接渲染,不再重跑一遍 `gh repo list`;缓存超过一分钟后先用它画出来、再在后台静默刷新,点「刷新」则永远真的去拉
|
|
920
920
|
- **信息更全的卡片** — 仓库名、描述,以及私有 / Fork / 语言 / 星标徽标,第三行再给最近推送日期、Fork 数、非 `main` 的默认分支与许可证(没有的项直接省略,不留占位)
|
|
921
921
|
- **直接克隆到文件夹** — 本地还没有的仓库提供「克隆到文件夹」:选好目录即可开始克隆,走 SSH 形式(`git@github.com:owner/repo.git`),遇到 `https://` 地址会先归一化,不会再卡在 Git Credential Manager 的账密弹窗上
|
|
922
|
-
- **「已克隆」徽标** — 服务端维护一份全盘本地 Git
|
|
922
|
+
- **「已克隆」徽标** — 服务端维护一份全盘本地 Git 仓库索引(后台构建、也可随时手动重扫),因此本地已有的仓库卡片会直接标出本地路径,而不是再让你克隆一遍;同一个仓库克隆到了多个文件夹时,徽标会标出条数(`已克隆 ×2`),悬浮提示列出全部路径,按住 Ctrl 点击会弹出菜单让你挑一处用 `g ui` 打开
|
|
923
923
|
- **一键打开 / 复制** — 点击卡片在浏览器打开仓库主页,悬浮时出现的按钮可复制地址或直接打开
|
|
924
924
|
|
|
925
925
|
---
|
|
@@ -1139,7 +1139,7 @@ Activity Bar 第四个视图,在 GUI 内直接浏览并编辑项目文件:
|
|
|
1139
1139
|
| 派发默认提示词 | 给每次派发配一段常驻提示词:一条**全局**的(所有项目都附加)+ 每个项目一条(派发到该项目时追加在全局之后,是补充而不是覆盖)。两者都在控制台输入区的齿轮按钮里设置;拼好的正文放在指令**之前**,「立即执行」旁边会多出一个「默认提示词(全局 + 本项目)」勾选,单次派发可以取消勾选,指令流水里也记下这条指令附带的是哪一级。提示词在派发那一刻就抄进任务自己的提示词字段,之后改设置不会回头改写已建任务,单条任务仍可再改 |
|
|
1140
1140
|
| 项目行打开方式 | 编排台项目列表里 hover 任意一行会出现两个按钮:「打开文件夹」一键进资源管理器,以及「打开方式」菜单 —— 文件管理器 / 终端 / 新标签页跑 `g ui`,以及各编辑器与 AI 工具(VS Code、Codex、OpenCode、Kimi Code、ZCode、DeepSeek Harness,加上默认权限或完全批准的 Claude Code)。没安装的工具会置灰并标「未安装」,点它弹的是顶栏那套安装引导;菜单里的动作只作用于该行项目,不会改变看板选中态 |
|
|
1141
1141
|
| 移除死项目 | 目录已经不存在的项目行仍会留在清单里(清单是常用目录与任务路径两份来源的并集),这一行 hover 时不再给任何打开动作(点了只会报「无法打开目录」),只留一颗红色的**「从清单移除」**。确认框会写明**任务不会被删除**,成功提示还会带上保留的条数。移除后这一行从清单消失,而任务、执行记录、历史一条不动;目录哪天被克隆回来,它会自己现身 |
|
|
1142
|
-
| 任务编辑器浮层 | 点击看板卡片时以浮层打开:左侧是扁平化的任务列表与提示词预置,右侧依次是任务头部、预置下拉、执行器 split 按钮、「复制执行内容 / 执行日志 /
|
|
1142
|
+
| 任务编辑器浮层 | 点击看板卡片时以浮层打开:左侧是扁平化的任务列表与提示词预置,右侧依次是任务头部、预置下拉、执行器 split 按钮、「复制执行内容 / 执行日志 / 清空执行」动作,以及对话式执行主体。描述默认折叠成一行「任务描述(可选)」摘要,点击展开;已填写描述或挂有附件时摘要右侧显示「已填写」徽标与附件数量。打开浮层时对话流直接落在**最新一轮**而不是第一轮:正文异步高亮与工具调用折叠会在挂载后继续长高,这段时间里持续贴底,用户自己一滚就立刻撒手。顶部细栏在「返回看板」与当前项目之后补一枚**任务状态徽标**(执行中 / 已完成 / 执行失败 / 待处理,用与看板列头同一套角色色,在跑时圆点脉动;悬停可看「AI 判定完成」「手动已完成」等依据 —— 以前细栏只有时间与用时,正在跑的任务顶上可能还挂着上一轮的「1 小时前 · 用时 13 分」,看着像早就跑完了),右端靠着一枚「任务执行」,并给出这条任务的**相对时间与用时**(跑过给「用时 x」,正在跑给实时的「已运行 x」,从没跑过只给时间、不写占位),与看板卡片上那两个值逐字一致;窗口收窄时先让位的是这一块与状态徽标,左边的返回 / 项目 / 执行于一个字节都不动(状态徽标在≈手机宽时整体收起,而不是压着项目名变形) |
|
|
1143
1143
|
| 复制执行内容 | 任务头部的「复制执行内容」把**整条任务**(所有轮次,不只是屏幕上那一段)以纯 Markdown 放进剪贴板:先是任务与项目的标题行,然后一轮一节(`## 第 N 轮`,下面一行是本轮执行器、状态与开始时间),每节依次是用户提示词、智能体思考、工具调用(参数与结果都放在围栏代码块里)与模型返回。内容是从执行记录现拼的、不是从 DOM 里抠的,所以复制到什么跟你当前选中了哪段文字无关。用户侧走的是与对话气泡同一套裁剪,复制出来的就是你真正说过的话 —— 注入的环境块 / 记忆块 / 附件清单留在 `job.prompt` 里不跟着出去。一个字都没有的轮次会被跳过,且不让后面的轮次跟着往前重编号;从没跑过的任务回答「暂无执行内容可复制」,而不是把空串写进剪贴板 |
|
|
1144
1144
|
| 执行器选择 | 每个任务可用 **Claude Code** / **OpenCode** / **Codex** 执行。全局默认在 **设置 → 通用设置 → 任务执行器**(`config.taskExecutor`);执行按钮旁的 split 按钮可临时切换下一次执行用的执行器,选择记在浏览器里。续聊固定沿用最初那个执行器 —— Claude 的 `--resume`、OpenCode 的 `--session`、Codex 的 thread id 互不通用 |
|
|
1145
1145
|
| 附件 | 附件**数量不设上限**(图片 / PDF / 文本 / Markdown / CSV / JSON / log,单个 ≤ 20 MB);超过 3.5 MB 的图片会先在浏览器里压缩(先按原分辨率转 WebP,压不下去再逐级降采样),4K 屏截图不用再手动裁剪;执行时绝对路径会自动追加到 prompt 末尾,智能体直接按路径读取。**右键图片附件可一键复制到系统剪贴板**(支持 png / jpeg / webp / gif)。看板的**新建任务弹窗**也能挂附件 —— 那一刻任务还不存在,所以文件先落在服务端暂存区(`workbench-images/_dispatch/`),点「创建」的瞬间被认领进任务自己的目录;还有附件正在上传时创建按钮是禁用的(否则那张图既进不了任务也不会被清掉),而关掉弹窗等于放弃这次新建,暂存的文件会一并删掉、不在磁盘上留一堆截图。文件名按百分号编码传输,所以中文名文件(「登录模块-改造前.png」)也能正常上传 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-gitsync",
|
|
3
|
-
"version": "2.18.
|
|
3
|
+
"version": "2.18.4",
|
|
4
4
|
"description": "Auto commit, scheduled sync, and visual GUI for Git. Run `g` in any repo for one-key commit & push, AI commit messages, scheduled background sync, and a drag-and-drop workflow builder.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
"verify:wb-open-with-permission": "node scripts/verify-open-with-permission-menus.cjs",
|
|
89
89
|
"verify:open-with-permission": "node --test test/open-with-permission.test.mjs",
|
|
90
90
|
"verify:repo-list": "node scripts/verify-repo-list.cjs",
|
|
91
|
+
"verify:repo-list:reverse": "node scripts/verify-repo-list.cjs --reverse",
|
|
91
92
|
"verify:startup-ai-panel": "node scripts/verify-startup-ai-panel.cjs",
|
|
92
93
|
"verify:startup-ai-panel:reverse": "node scripts/verify-startup-ai-panel.cjs --reverse",
|
|
93
94
|
"verify:startup-ai-panel:empty": "node scripts/verify-startup-ai-panel.cjs --empty",
|
|
@@ -192,7 +193,7 @@
|
|
|
192
193
|
},
|
|
193
194
|
"homepage": "https://github.com/xz333221/zen-gitsync#readme",
|
|
194
195
|
"dependencies": {
|
|
195
|
-
"ai-model-form": "^0.3.
|
|
196
|
+
"ai-model-form": "^0.3.3",
|
|
196
197
|
"boxen": "^8.0.1",
|
|
197
198
|
"chalk": "^5.4.1",
|
|
198
199
|
"cli-table3": "^0.6.5",
|
package/src/cli/ai/modelSetup.js
CHANGED
|
@@ -62,9 +62,21 @@ export const PROVIDERS = [
|
|
|
62
62
|
{ id: 'opencode', label: 'OpenCode Go', url: 'https://opencode.ai/zen/go/v1' },
|
|
63
63
|
{ id: 'commandcode', label: 'Command Code', url: 'https://api.commandcode.ai/provider/v1' },
|
|
64
64
|
{ id: 'agnes', label: 'Agnes AI', url: 'https://apihub.agnes-ai.com/v1' },
|
|
65
|
+
// 火山方舟订阅套餐:同域名不同数据面路径(/api/plan/v3 与 /api/coding/v3),填错直接 404
|
|
66
|
+
{ id: 'ark-agent-plan', label: '火山方舟 Agent Plan', url: 'https://ark.cn-beijing.volces.com/api/plan/v3' },
|
|
67
|
+
{ id: 'ark-coding-plan', label: '火山方舟 Coding Plan', url: 'https://ark.cn-beijing.volces.com/api/coding/v3' },
|
|
65
68
|
{ id: 'ollama', label: 'Ollama (本地)', url: 'http://localhost:11434/v1' },
|
|
66
69
|
]
|
|
67
70
|
|
|
71
|
+
// ──────────────────────────────────────────────
|
|
72
|
+
// 火山方舟两个订阅套餐的端点
|
|
73
|
+
// Agent Plan → /api/plan/v3
|
|
74
|
+
// Coding Plan → /api/coding/v3
|
|
75
|
+
// 同域名不同数据面路径,填错直接 404;两边都只认套餐内的模型名。
|
|
76
|
+
// ──────────────────────────────────────────────
|
|
77
|
+
export const ARK_AGENT_PLAN_URL = 'https://ark.cn-beijing.volces.com/api/plan/v3'
|
|
78
|
+
export const ARK_CODING_PLAN_URL = 'https://ark.cn-beijing.volces.com/api/coding/v3'
|
|
79
|
+
|
|
68
80
|
// ──────────────────────────────────────────────
|
|
69
81
|
// 内置常用模型列表
|
|
70
82
|
// 来源:ai-model-form 包 server/middleware.js 的 BUILTIN_MODELS
|
|
@@ -180,6 +192,63 @@ export const BUILTIN_MODELS = {
|
|
|
180
192
|
'claude-opus-5-5', 'claude-opus-5', 'claude-opus-4-8',
|
|
181
193
|
'claude-haiku-5-5', 'claude-haiku-4-5-20251001',
|
|
182
194
|
],
|
|
195
|
+
// 火山方舟套餐 — 两个端点都只认套餐内的模型名(不是自建 Endpoint ID),
|
|
196
|
+
// 且它们的 /models 返回的是方舟全量目录(含向量/视频/已下线模型)而不是套餐清单,
|
|
197
|
+
// 所以这两个端点走 BUILTIN_ONLY_ENDPOINTS:不拉实时列表,只用下面的实测清单。
|
|
198
|
+
// 更新: 2026-10-10,用真实套餐 Key 逐个请求核实,与 ai-model-form 同步。
|
|
199
|
+
[ARK_AGENT_PLAN_URL]: [
|
|
200
|
+
'doubao-seed-2-1-pro-260915', 'doubao-seed-2-1-pro-260628', 'doubao-seed-2-1-turbo-260628',
|
|
201
|
+
'doubao-seed-2-1-lite-260915',
|
|
202
|
+
'doubao-seed-2-0-pro-260215', 'doubao-seed-2-0-lite-260428', 'doubao-seed-2-0-lite-260215',
|
|
203
|
+
'doubao-seed-2-0-mini-260428', 'doubao-seed-2-0-mini-260215',
|
|
204
|
+
'doubao-seed-2-0-code-preview-260215', 'doubao-seed-evolving',
|
|
205
|
+
'deepseek-v4-pro-ga-260813', 'deepseek-v4-pro-260425',
|
|
206
|
+
'deepseek-v4-1-flash-260910', 'deepseek-v4-flash-ga-260731',
|
|
207
|
+
'glm-5-3-flash-260828', 'glm-5-2-260617',
|
|
208
|
+
],
|
|
209
|
+
[ARK_CODING_PLAN_URL]: [
|
|
210
|
+
'doubao-seed-code', 'doubao-seed-evolving',
|
|
211
|
+
'doubao-seed-2-1-pro-260915', 'doubao-seed-2-1-pro-260628', 'doubao-seed-2-1-turbo-260628',
|
|
212
|
+
'doubao-seed-2-1-lite-260915',
|
|
213
|
+
'doubao-seed-2-0-pro-260215', 'doubao-seed-2-0-mini-260428', 'doubao-seed-2-0-mini-260215',
|
|
214
|
+
'doubao-seed-2-0-code-preview-260215',
|
|
215
|
+
'deepseek-v4-pro-ga-260813', 'deepseek-v4-pro-260425',
|
|
216
|
+
'deepseek-v4-1-flash-260910', 'deepseek-v4-flash-ga-260731',
|
|
217
|
+
'glm-5-3-flash-260828', 'glm-5-2-260617',
|
|
218
|
+
'kimi-k2-thinking-251104', 'kimi-k2-turbo-preview-250905', 'kimi-k2-250905',
|
|
219
|
+
],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ──────────────────────────────────────────────
|
|
223
|
+
// 火山方舟套餐的辅助判断(端点常量见文件顶部)
|
|
224
|
+
// ──────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 这些端点的 /models 不能用来填候选列表(返回的是全量目录,不是可用的模型集合),
|
|
228
|
+
* 必须只用 BUILTIN_MODELS。与 ai-model-form 的 ARK_PLAN_URLS 保持同步。
|
|
229
|
+
*/
|
|
230
|
+
export const BUILTIN_ONLY_ENDPOINTS = new Set([ARK_AGENT_PLAN_URL, ARK_CODING_PLAN_URL])
|
|
231
|
+
|
|
232
|
+
/** 归一化 baseURL(去尾斜杠),用于和上面的常量比对 */
|
|
233
|
+
function normalizeEndpoint(baseURL) {
|
|
234
|
+
return String(baseURL || '').trim().replace(/\/+$/, '')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** 套餐端点的展示名 */
|
|
238
|
+
function arkPlanLabel(baseURL) {
|
|
239
|
+
return normalizeEndpoint(baseURL) === ARK_CODING_PLAN_URL ? 'Coding Plan' : 'Agent Plan'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 套餐端点把"不在套餐里"表达成 404 UnsupportedModel,和"模型名写错"共用同一个状态码,
|
|
244
|
+
* 所以只能认那句专属文案,不能只看状态码。
|
|
245
|
+
* @param {{code?: string, message?: string}} err - readErrorBody 的结果
|
|
246
|
+
* @param {'agent'|'coding'} plan
|
|
247
|
+
*/
|
|
248
|
+
function isArkPlanModelError(err, plan) {
|
|
249
|
+
const blob = `${err?.code || ''} ${err?.message || ''}`
|
|
250
|
+
// 错误码是 camelCase 的 UnsupportedModel,两种写法都认
|
|
251
|
+
return /unsupported[_\s-]?model/i.test(blob) && new RegExp(`does not support the ${plan} plan`, 'i').test(blob)
|
|
183
252
|
}
|
|
184
253
|
|
|
185
254
|
// ──────────────────────────────────────────────
|
|
@@ -279,6 +348,16 @@ export function getBuiltinModels(baseURL) {
|
|
|
279
348
|
return BUILTIN_MODELS[normalized] || []
|
|
280
349
|
}
|
|
281
350
|
|
|
351
|
+
/**
|
|
352
|
+
* 该端点是否只能靠内置清单列候选模型(实时 /models 返回的不是可用模型集合)。
|
|
353
|
+
* @param {string} baseURL
|
|
354
|
+
* @returns {boolean}
|
|
355
|
+
*/
|
|
356
|
+
export function isBuiltinOnlyEndpoint(baseURL) {
|
|
357
|
+
if (!baseURL || typeof baseURL !== 'string') return false
|
|
358
|
+
return BUILTIN_ONLY_ENDPOINTS.has(baseURL.trim().replace(/\/+$/, ''))
|
|
359
|
+
}
|
|
360
|
+
|
|
282
361
|
/**
|
|
283
362
|
* 根据 URL 查找匹配的预设 provider。
|
|
284
363
|
* @param {string} url
|
|
@@ -438,19 +517,47 @@ export async function testModelConnection({ baseURL, model, apiKey, timeoutMs =
|
|
|
438
517
|
if (res.status === 400) {
|
|
439
518
|
const err = await readErrorBody(res)
|
|
440
519
|
const blob = `${err.code} ${err.message}`
|
|
441
|
-
if (/
|
|
520
|
+
if (/unsupported[_\s-]?model|must be called via|is not supported on this endpoint/i.test(blob)) {
|
|
442
521
|
return {
|
|
443
522
|
ok: false,
|
|
444
523
|
message: `模型 "${model}" 在该接口不可用 (400)${err.message ? ':' + err.message : ''}`,
|
|
445
524
|
status: 400,
|
|
446
525
|
}
|
|
447
526
|
}
|
|
527
|
+
// 限流 / 额度类 400 不是"探活参数太小被拒",不能算连接成功
|
|
528
|
+
if (/rate.?limit|quota|throttl|too many|overload|busy|concurrenc/i.test(blob)) {
|
|
529
|
+
return {
|
|
530
|
+
ok: false,
|
|
531
|
+
message: `接口限流或额度不足 (400)${err.message ? ':' + err.message : ''}`,
|
|
532
|
+
status: 400,
|
|
533
|
+
}
|
|
534
|
+
}
|
|
448
535
|
return { ok: true, message: 'OK', status: 400 }
|
|
449
536
|
}
|
|
450
537
|
if (res.status === 401) {
|
|
538
|
+
if (BUILTIN_ONLY_ENDPOINTS.has(normalizeEndpoint(baseURL))) {
|
|
539
|
+
return {
|
|
540
|
+
ok: false,
|
|
541
|
+
message: `API Key 无效或与套餐不匹配 (401):${arkPlanLabel(baseURL)} 需要该套餐下发的 Key`,
|
|
542
|
+
status: 401,
|
|
543
|
+
}
|
|
544
|
+
}
|
|
451
545
|
return { ok: false, message: 'API Key 无效或未授权 (401)', status: 401 }
|
|
452
546
|
}
|
|
453
547
|
if (res.status === 404) {
|
|
548
|
+
// 套餐端点用 404 表达"模型不在套餐内",和"模型名不存在"是同一个状态码
|
|
549
|
+
if (BUILTIN_ONLY_ENDPOINTS.has(normalizeEndpoint(baseURL))) {
|
|
550
|
+
const err = await readErrorBody(res)
|
|
551
|
+
const label = arkPlanLabel(baseURL)
|
|
552
|
+
if (isArkPlanModelError(err, normalizeEndpoint(baseURL) === ARK_CODING_PLAN_URL ? 'coding' : 'agent')) {
|
|
553
|
+
return {
|
|
554
|
+
ok: false,
|
|
555
|
+
message: `${label} 套餐不含模型 "${model}":请从内置清单里选套餐内的模型,`
|
|
556
|
+
+ `或在火山方舟控制台确认 ${label} 的可用模型清单`,
|
|
557
|
+
status: 404,
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
454
561
|
return { ok: false, message: `模型 "${model}" 不存在 (404)`, status: 404 }
|
|
455
562
|
}
|
|
456
563
|
return { ok: false, message: `服务器返回错误: ${res.status}`, status: res.status }
|
|
@@ -827,15 +934,21 @@ export async function collectModelInput({ locale = 'zh-CN', rl: injectedRl, fetc
|
|
|
827
934
|
const apiKey = await asker.ask(t.apiKeyPrompt)
|
|
828
935
|
|
|
829
936
|
// 4. 选择/输入模型名称
|
|
830
|
-
// 先尝试从 API 获取模型列表(有了 apiKey 才能调通)
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
937
|
+
// 先尝试从 API 获取模型列表(有了 apiKey 才能调通)。
|
|
938
|
+
// 例外:套餐端点的 /models 返回的是全量目录而不是套餐清单,拉回来会让用户
|
|
939
|
+
// 以为套餐里能调 qwen3/seedream,所以这类端点直接跳过请求,只用内置清单。
|
|
940
|
+
const builtinOnly = isBuiltinOnlyEndpoint(baseURL)
|
|
941
|
+
let fetchedModels = []
|
|
942
|
+
if (!builtinOnly) {
|
|
943
|
+
const fetchSpinner = startSpinner(t.fetchingModels)
|
|
944
|
+
fetchedModels = await fetchModelsFromApi({ baseURL, apiKey, fetchFn })
|
|
945
|
+
fetchSpinner.stop()
|
|
946
|
+
}
|
|
834
947
|
|
|
835
948
|
// 使用 API 返回的模型列表,如果失败则使用内置列表
|
|
836
949
|
const modelList = fetchedModels.length > 0 ? fetchedModels : getBuiltinModels(baseURL)
|
|
837
950
|
|
|
838
|
-
if (fetchedModels.length === 0 && getBuiltinModels(baseURL).length > 0) {
|
|
951
|
+
if (!builtinOnly && fetchedModels.length === 0 && getBuiltinModels(baseURL).length > 0) {
|
|
839
952
|
console.log(chalk.dim(` ${t.fetchModelsFailed}`))
|
|
840
953
|
}
|
|
841
954
|
|
|
@@ -968,4 +1081,4 @@ export async function runModelSetup({ locale = 'zh-CN', rl: injectedRl, fetchFn
|
|
|
968
1081
|
}
|
|
969
1082
|
}
|
|
970
1083
|
|
|
971
|
-
export default { runModelSetup, collectModelInput, fetchModelsFromApi, testModelConnection, getBuiltinModels, findProviderByUrl, validateEndpoint, buildModelConfig, PROVIDERS, BUILTIN_MODELS }
|
|
1084
|
+
export default { runModelSetup, collectModelInput, fetchModelsFromApi, testModelConnection, getBuiltinModels, isBuiltinOnlyEndpoint, findProviderByUrl, validateEndpoint, buildModelConfig, PROVIDERS, BUILTIN_MODELS }
|
|
@@ -20,6 +20,7 @@ import assert from 'node:assert/strict'
|
|
|
20
20
|
import { EventEmitter } from 'node:events'
|
|
21
21
|
import {
|
|
22
22
|
getBuiltinModels,
|
|
23
|
+
isBuiltinOnlyEndpoint,
|
|
23
24
|
findProviderByUrl,
|
|
24
25
|
validateEndpoint,
|
|
25
26
|
buildModelConfig,
|
|
@@ -267,6 +268,80 @@ test('testModelConnection: 404 模型不存在', async () => {
|
|
|
267
268
|
assert.match(result.message, /404/)
|
|
268
269
|
})
|
|
269
270
|
|
|
271
|
+
test('testModelConnection: 400 限流/额度不足 → 报失败,不能当成"连接成功"', async () => {
|
|
272
|
+
const result = await testModelConnection({
|
|
273
|
+
baseURL: 'https://api.deepseek.com/v1',
|
|
274
|
+
model: 'deepseek-chat',
|
|
275
|
+
fetchFn: mockFetch({
|
|
276
|
+
status: 400,
|
|
277
|
+
ok: false,
|
|
278
|
+
body: { error: { code: 'RateLimitExceeded', message: 'too many requests' } },
|
|
279
|
+
}),
|
|
280
|
+
})
|
|
281
|
+
assert.equal(result.ok, false)
|
|
282
|
+
assert.match(result.message, /限流或额度不足/)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// ── 火山方舟套餐:错误语义映射 ──────────────────
|
|
286
|
+
|
|
287
|
+
test('testModelConnection: 套餐端点 404=不在套餐内,不能说成"模型不存在"', async () => {
|
|
288
|
+
const result = await testModelConnection({
|
|
289
|
+
baseURL: 'https://ark.cn-beijing.volces.com/api/coding/v3',
|
|
290
|
+
model: 'qwen3-8b-20250429',
|
|
291
|
+
apiKey: 'ark-test',
|
|
292
|
+
fetchFn: mockFetch({
|
|
293
|
+
status: 404,
|
|
294
|
+
ok: false,
|
|
295
|
+
body: {
|
|
296
|
+
error: {
|
|
297
|
+
code: 'UnsupportedModel',
|
|
298
|
+
message: 'The requested model does not support the coding plan feature.',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
})
|
|
303
|
+
assert.equal(result.ok, false)
|
|
304
|
+
assert.match(result.message, /Coding Plan 套餐不含模型/)
|
|
305
|
+
assert.doesNotMatch(result.message, /不存在/)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('testModelConnection: Agent Plan 端点的 404 指向 Agent Plan', async () => {
|
|
309
|
+
const result = await testModelConnection({
|
|
310
|
+
baseURL: 'https://ark.cn-beijing.volces.com/api/plan/v3',
|
|
311
|
+
model: 'doubao-seed-code',
|
|
312
|
+
apiKey: 'ark-test',
|
|
313
|
+
fetchFn: mockFetch({
|
|
314
|
+
status: 404,
|
|
315
|
+
ok: false,
|
|
316
|
+
body: {
|
|
317
|
+
error: {
|
|
318
|
+
code: 'UnsupportedModel',
|
|
319
|
+
message: 'The requested model does not support the agent plan feature.',
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
}),
|
|
323
|
+
})
|
|
324
|
+
assert.equal(result.ok, false)
|
|
325
|
+
assert.match(result.message, /Agent Plan 套餐不含模型/)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('testModelConnection: 套餐端点 401 提示 Key 与套餐不匹配', async () => {
|
|
329
|
+
const result = await testModelConnection({
|
|
330
|
+
baseURL: 'https://ark.cn-beijing.volces.com/api/plan/v3',
|
|
331
|
+
model: 'doubao-seed-2-0-pro-260215',
|
|
332
|
+
apiKey: 'ark-coding-key',
|
|
333
|
+
fetchFn: mockFetch({
|
|
334
|
+
status: 401,
|
|
335
|
+
ok: false,
|
|
336
|
+
body: { error: { code: 'AuthenticationError', message: 'invalid' } },
|
|
337
|
+
}),
|
|
338
|
+
})
|
|
339
|
+
assert.equal(result.ok, false)
|
|
340
|
+
assert.equal(result.status, 401)
|
|
341
|
+
assert.match(result.message, /Agent Plan/)
|
|
342
|
+
assert.match(result.message, /套餐不匹配/)
|
|
343
|
+
})
|
|
344
|
+
|
|
270
345
|
test('testModelConnection: 500 服务器错误', async () => {
|
|
271
346
|
const result = await testModelConnection({
|
|
272
347
|
baseURL: 'https://api.deepseek.com/v1',
|
|
@@ -386,6 +461,52 @@ test('BUILTIN_MODELS: 每个 key 都能在 PROVIDERS 中找到', () => {
|
|
|
386
461
|
}
|
|
387
462
|
})
|
|
388
463
|
|
|
464
|
+
// ── 火山方舟套餐(Agent Plan / Coding Plan) ────
|
|
465
|
+
|
|
466
|
+
const ARK_AGENT_PLAN_URL = 'https://ark.cn-beijing.volces.com/api/plan/v3'
|
|
467
|
+
const ARK_CODING_PLAN_URL = 'https://ark.cn-beijing.volces.com/api/coding/v3'
|
|
468
|
+
|
|
469
|
+
test('火山方舟: 两个套餐都有预设且路径不同(填错会 404)', () => {
|
|
470
|
+
const agent = findProviderByUrl(ARK_AGENT_PLAN_URL)
|
|
471
|
+
const coding = findProviderByUrl(ARK_CODING_PLAN_URL)
|
|
472
|
+
assert.ok(agent, 'Agent Plan 应有预设')
|
|
473
|
+
assert.ok(coding, 'Coding Plan 应有预设')
|
|
474
|
+
assert.notEqual(agent.url, coding.url)
|
|
475
|
+
// 同域名、不同数据面路径:这是最容易填错的地方,锁住
|
|
476
|
+
assert.equal(new URL(agent.url).hostname, new URL(coding.url).hostname)
|
|
477
|
+
assert.equal(new URL(agent.url).pathname, '/api/plan/v3')
|
|
478
|
+
assert.equal(new URL(coding.url).pathname, '/api/coding/v3')
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
test('火山方舟: 两个套餐都标为"只用内置清单"(/models 返回的是全量目录)', () => {
|
|
482
|
+
assert.equal(isBuiltinOnlyEndpoint(ARK_AGENT_PLAN_URL), true)
|
|
483
|
+
assert.equal(isBuiltinOnlyEndpoint(ARK_CODING_PLAN_URL), true)
|
|
484
|
+
assert.equal(isBuiltinOnlyEndpoint(ARK_AGENT_PLAN_URL + '/'), true, '尾斜杠也要认')
|
|
485
|
+
assert.equal(isBuiltinOnlyEndpoint('https://api.deepseek.com/v1'), false)
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test('火山方舟: 两个套餐的内置清单非空且互有差异', () => {
|
|
489
|
+
const agent = getBuiltinModels(ARK_AGENT_PLAN_URL)
|
|
490
|
+
const coding = getBuiltinModels(ARK_CODING_PLAN_URL)
|
|
491
|
+
assert.ok(agent.length > 0)
|
|
492
|
+
assert.ok(coding.length > 0)
|
|
493
|
+
// doubao-seed-code 只在 Coding Plan(实测 Agent Plan 回 UnsupportedModel),
|
|
494
|
+
// 两边清单如果被同步成一份,这条会红
|
|
495
|
+
assert.ok(coding.includes('doubao-seed-code'))
|
|
496
|
+
assert.ok(!agent.includes('doubao-seed-code'))
|
|
497
|
+
// kimi 系同样只在 Coding Plan
|
|
498
|
+
assert.ok(coding.includes('kimi-k2-250905'))
|
|
499
|
+
assert.ok(!agent.includes('kimi-k2-250905'))
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
test('火山方舟: 清单里不含全量目录里的非套餐模型', () => {
|
|
503
|
+
const all = [...getBuiltinModels(ARK_AGENT_PLAN_URL), ...getBuiltinModels(ARK_CODING_PLAN_URL)]
|
|
504
|
+
// /models 返回的全量目录里有这些,但它们不在套餐内,不该出现在候选里
|
|
505
|
+
for (const bad of ['qwen3-8b-20250429', 'doubao-seedream-4-0-250828', 'doubao-embedding-text-240515']) {
|
|
506
|
+
assert.ok(!all.includes(bad), `套餐清单不应包含 ${bad}`)
|
|
507
|
+
}
|
|
508
|
+
})
|
|
509
|
+
|
|
389
510
|
// ── collectModelInput:交互式收集(注入 mock rl + mock fetch) ──
|
|
390
511
|
//
|
|
391
512
|
// collectModelInput 是 runModelSetup 和 agent.js /addmodel 共用的核心交互逻辑。
|
|
@@ -565,6 +686,42 @@ test('collectModelInput: Ctrl+C 只取消当前问题并阻止 readline 关闭',
|
|
|
565
686
|
}
|
|
566
687
|
})
|
|
567
688
|
|
|
689
|
+
test('collectModelInput: 套餐端点不拉 /models,直接用内置清单', async () => {
|
|
690
|
+
const restore = silenceConsole()
|
|
691
|
+
try {
|
|
692
|
+
const calls = []
|
|
693
|
+
// /models 故意返回一个"看起来能用"的全量目录,如果被采信,model 就会是 api-model-1
|
|
694
|
+
const spyFetch = async (url) => {
|
|
695
|
+
calls.push(String(url))
|
|
696
|
+
if (String(url).endsWith('/models')) {
|
|
697
|
+
return {
|
|
698
|
+
ok: true,
|
|
699
|
+
status: 200,
|
|
700
|
+
json: async () => ({ data: [{ id: 'api-model-1' }, { id: 'api-model-2' }] }),
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return { ok: true, status: 200, json: async () => ({}) }
|
|
704
|
+
}
|
|
705
|
+
// 选 Coding Plan(21) → 确认 baseURL → apiKey → 选内置清单第 1 个 → displayName
|
|
706
|
+
const rl = createMockRl(['21', '', 'ark-test-key', '1', ''])
|
|
707
|
+
const result = await collectModelInput({
|
|
708
|
+
locale: 'zh-CN',
|
|
709
|
+
rl,
|
|
710
|
+
fetchFn: spyFetch,
|
|
711
|
+
})
|
|
712
|
+
assert.ok(result)
|
|
713
|
+
assert.equal(result.baseURL, ARK_CODING_PLAN_URL)
|
|
714
|
+
assert.ok(
|
|
715
|
+
!calls.some((u) => u.endsWith('/models')),
|
|
716
|
+
`套餐端点不该请求 /models,实际请求: ${calls.join(', ')}`
|
|
717
|
+
)
|
|
718
|
+
assert.equal(result.model, getBuiltinModels(ARK_CODING_PLAN_URL)[0], '应来自内置清单')
|
|
719
|
+
assert.equal(result.apiKey, 'ark-test-key')
|
|
720
|
+
} finally {
|
|
721
|
+
restore()
|
|
722
|
+
}
|
|
723
|
+
})
|
|
724
|
+
|
|
568
725
|
test('collectModelInput: API 返回模型列表时优先使用 → 返回收集的字段', async () => {
|
|
569
726
|
const restore = silenceConsole()
|
|
570
727
|
try {
|