@suifei/dsh-ai-team 0.3.3 → 0.3.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.5 — 2026-11-09
4
+
5
+ - 新 Project 默认成员:沈舟(主理人)/ 林岚(执行)/ 顾清(验收),均可在成员资料中改;
6
+ - 默认 Lead 成为 Project owner,让一句话开工可以直接唤醒团队;
7
+ - Provider / Model 改为读取 DSH `llm.listProviders()` + `llm.listModels()` 的下拉,不再手填 id;
8
+ - 新增 `GET /ai-team/api/models` 与 `lib/host/model-catalog.js`;
9
+ - 默认成员只在 Project 没有 Agent 时播种一次,不覆盖用户已有 roster。
10
+
11
+ ## 0.3.4 — 2026-11-09
12
+
13
+ - README 精装修:品牌、快速开始、能力地图、架构图、API、验证证据、文档索引;
14
+ - 发布到 npm / GitHub Releases,补充 repository / homepage / keywords / LICENSE;
15
+ - desktop profile 改为从 npm 安装。
16
+
3
17
  ## 0.3.3 — 2026-11-09
4
18
 
5
19
  - 产品显示名改为「文渊阁」(Wenyuan Pavilion),取藏修、校勘、纂辑之意;
package/README.md CHANGED
@@ -1,39 +1,231 @@
1
- # 文渊阁 · @suifei/dsh-ai-team
1
+ <p align="center">
2
+ <img src="./icon.svg" width="88" height="88" alt="文渊阁" />
3
+ </p>
2
4
 
3
- > 文渊阁:藏修、校勘、纂辑。AI Team mode for DeepSeek Harness:
5
+ <h1 align="center">文渊阁</h1>
4
6
 
5
- AI Team mode for DeepSeek Harness: **projects, owned tasks, threads, inbox and an append-only team ledger** — one Human sentence, a team of named persistent agents keeps going, and only direction / trust / irreversible steps come back.
7
+ <p align="center">
8
+ <strong>把工作交给一支 AI 团队:它们把事做到底,你定方向、把关键的关。</strong>
9
+ </p>
6
10
 
7
- > M1/M2 status: the package mounts a **three-pane workbench** (left projects + agents, center `主对话 / 看板 / 态势`, right thread). The app ledger, task/thread/inbox domain and `TeamService` are implemented and covered by `node --test` (82 tests); `bootstrap`, `POST .../commands`, `GET .../board`, `GET .../quadrants` and the event-driven `GET .../events` SSE are wired to a real default-project ledger. The profile patch enforces `danger-full-access`, and the Host refuses to boot a weaker profile; `member.activate` now creates a real continuable child through the Team Host Agent, binds the session, sets/verifies `danger-full-access`, and records it in the ledger. Real activation and `member.config.update` were verified on the ai-team profile; the Host subscribes to `agent/request-error` for classification/retry/fallback (auth/billing fail closed) and to `session/event` for recovery/reset; the member row shows effective model/context/version/last failure. A member-only `team` tool exposes tasks/inbox/claim/submit/thread/reply/review through `TeamService`; the Waker delivers wake messages and records `wake/delivered`/`wake/failed`. The direct `agents.create + followup` path now completes a real two-Agent loop: 沈舟 claimed Task #1, replied in the Thread and submitted; 林岚 independently accepted it (`reviewerId=a2`, `selfExecuted=false`), leaving the task `done`. A follow-up probe wrote one Human sentence into the Main stream; Lead a1 created Task #1 itself, claimed/executed/submitted it, and a2 independently accepted it. The workbench Composer is wired to the real `stream.post` / `task.create` commands, and the Agents “+” opens an Add Member form that creates, configures, activates and optionally promotes the member to Project owner. A headless-Edge E2E opened 文渊阁, added 沈舟 as owner through the form, sent one sentence, and saw Task #1 appear on the board and in Q1. A second form submission added 林岚3 as reviewer with an explicit accept instruction; the board then showed `Done 2` / `To accept 0`. Host now broadcasts SSE on every ledger append, including member `team` tool calls. M2.5 adds versioned member profiles (name/role/personality), Agent self-evolution via `team.evolve`, a talent pool with leave/rejoin, leftover-task takeover, and QA-first automatic reviewer wake; a real QA member accepted a submitted Task with no manual reviewer instruction. The profile/talent-pool UI was verified in a real browser: saving a new name/personality bumped `profileVersion`, then leaving the team with a successor moved the member to the talent pool and transferred the leftover Task (`taken-over`) to the successor. Acceptance also schedules a reflection wake; a real Agent then called `team.evolve` itself and wrote `member/distilled`, with the new personality visible on the member. Members can add durable notes/skills (`team.note`/`team.skill`); skill bodies are read from `<dataDir>/skills/<name>/SKILL.md` (bounded, traversal-safe) and injected into activation/wake prompts. Activation/wake also injects a bounded DSH `skills` catalog (name/description/when-to-use, max 12) so members can load a full skill through their own `skill` tool. Members now mount an agent preset in `agents.create` setup; a real member used `pwsh` and then `skill` to load `diagnose-windows-sandbox-acl` and wrote the result to a file. The profile drawer can choose `standard / ptc / minimal / cordis` (default `standard`). All four were exercised end-to-end: PTC used `run_code`, minimal and cordis used `pwsh`, standard used `pwsh` + `skill`. `team.recall` searches their own notes/skills/tasks/threads; a Lead can inspect a colleague (`team.member`) and call `team.distill`. Host now has a narrow single-step distillation path: `member.distill(mode:'model')` feeds the member's profile, notes, skills and recent tasks to `llm.stream` with no tools, requires JSON output, then records `member/distilled`. A real run produced a new personality from the old one plus a missed-gate note. Free-form Lead-driven distillation can still drift; the Host path is the product entry. Restart persistence was verified: the same member/session resumed from 63 persisted events (31 zstd frames), and a post-restart Task was claimed and submitted without re-onboarding. The archived-host gate was fixed by not marking members as `origin:'subagent'`. A real 401 also produces `member/llm-failed(auth)`, member `failed`, a Human `decision_required`, and leaves the task pending without retry. See `docs/reports/M2-真实闭环验收.md`. The workbench Thread now sends real `thread.post`, `task.accept` and `task.request_changes`; the task action row also drives `task.cancel`, `task.reopen`, `task.delete` and `task.reassign` (the “demo only” tag is gone). A headless-Edge run accepted a pending Task to `done` from the UI. A Human can DM one member from the profile drawer (`member.message`), which writes to the ledger and wakes only that member; a headless-Edge run sent a DM and verified the member session advanced. The first profile-drawer E2E also exposed and fixed a white-screen bug (`selectedTask` referenced across an early return / null `data`). The Inbox page lists real `data.inbox` decisions and acknowledges them through `inbox.ack`. Follow/unfollow is persisted through `task.follow`; accepting a followed Task creates one `followed_task_update` report for each still-following member. A headless-Edge failure-recovery run used a bad provider to produce a Human decision, acknowledged it, edited the member's provider/maxTokens in the profile drawer, and retried the member back to `running` (`member/recovered`). The latest regression re-ran one sentence → Lead opens a card → QA auto-accept → owner reflection/self-evolution with one wake per stage (20 ledger events, no duplicate human/reflection wakes). On Host startup, unfinished Tasks are woken once via a `team/startup` event (no timers). A real restart test resumed a pending Task: `wake/delivered reason=startup_resume`, then claim → Thread → submit to pending acceptance (15 ledger events; tail repair recorded). The profile drawer now edits provider/model/maxTokens, adds notes/skills, and retries failed members. Task creation can mark `qaRequired` and a `qaChecklist`; non-QA Agents are rejected on accept, the Waker only offers the task to QA, and QA must pass every checklist item before acceptance. M3 adds a test-only `ai-team-fault` LLM adapter for real billing/network failure injection, cross-project Inbox (including opening a task from another project), and a filesystem SkillProvider for installed skills. SSE buckets were verified with two real providers (DeepSeek and Zhipu) producing events in two Projects. Microsoft SkillOpt's seven tools, Python engine and `skillopt-sleep` skill are bundled inside this plugin, not installed into the user's DSH. Version 0.3.3 (文渊阁 / Wenyuan Pavilion; concurrent Projects + cross-project inbox + V-10 guard E2E + billing/network injection + bundled SkillOpt; health reports 0.3.3). Install/uninstall was exercised on a throwaway `ai-team-smoke` profile: health 200 after install, 404 after removal, config clean. — see `CHANGELOG.md`, `docs/05-项目启动与执行计划.md`, `docs/08-异常恢复与模型治理.md` and `docs/reports/M1-验收矩阵.md`.
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@suifei/dsh-ai-team"><img alt="npm" src="https://img.shields.io/npm/v/@suifei/dsh-ai-team?color=2f6feb&label=npm"></a>
13
+ <a href="https://github.com/suifei/dsh-ai-team/releases"><img alt="release" src="https://img.shields.io/github/v/release/suifei/dsh-ai-team?color=2f6feb"></a>
14
+ <a href="./LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-2f6feb"></a>
15
+ <img alt="tests" src="https://img.shields.io/badge/tests-82%20passing-2ea043">
16
+ <img alt="dsh" src="https://img.shields.io/badge/DeepSeek%20Harness-plugin-6f42c1">
17
+ </p>
8
18
 
9
- ## Install (dedicated profile)
19
+ > 文渊阁,藏修、校勘、纂辑之地。
20
+ > 在这里,Human 不在环里逐条调度,而是在环上定方向、守信任、做最后那一步。
10
21
 
11
- ```sh
12
- dsh --profile ai-team --from-default-profile web --dump-config # create the profile from the web template
13
- dsh plugin --profile ai-team add @suifei/dsh-ai-team # install from npm
14
- # or from a checkout:
15
- # dsh plugin --profile ai-team add E:/works/dsh-ai-team
16
- dsh --profile ai-team --no-open --port 0 # boot
22
+ `@suifei/dsh-ai-team` 是 DeepSeek Harness 的按需启用插件:装进专用 `ai-team` profile 后,DSH 里出现一个三栏工作台——左侧 Project 与成员,中间主对话 / 看板 / 态势,右侧 Thread。Agent 自己认领 Task、互相交接,由做的人以外的成员验收;Human 只处理方向、信任和不可撤回的决定。
23
+
24
+ 新 Project 自带一组可改的默认成员:**沈舟(主理人)**、**林岚(执行)**、**顾清(验收)**。名字、职责、个性、模型都可以改;模型提供商和模型名称直接读取 DSH 的 `llm` 目录,不需要手填 id。
25
+
26
+ ---
27
+
28
+ ## 为什么是「文渊阁」
29
+
30
+ - **成员是持久的。** 每个 Agent 有名字、角色、personality、notes、skills 和 session;DSH 重启、上下文压缩、换代之后,它还是它。
31
+ - **每个结果都有 owner。** Project 有具名 owner;Task 有唯一 accountable owner,由它推到验收。
32
+ - **做的人不验收。** Agent 只能验收已进入待验收、且自己从未认领过的 Task;QA Task 只有 QA 成员能验收。
33
+ - **房间是为 Agent 设计的。** 成员从自己的 inbox 拉取消息,而不是被每条消息推着走;对着已经变化的 Thread 写的回复会被退回,并附上变化摘要。
34
+ - **一切有记录。** Main stream、Thread、Task、收件箱都是同一本 append-only 账本的视图。
35
+ - **没有定时器。** Agent 只由 Team 事件(消息、@、交接、验收请求)唤醒;团队完全安静时不会自己醒来。
36
+
37
+ ---
38
+
39
+ ## 快速开始
40
+
41
+ ### 1. 建专用 profile
42
+
43
+ ```powershell
44
+ dsh --profile ai-team --from-default-profile web --dump-config
45
+ ```
46
+
47
+ ### 2. 从 npm 安装
48
+
49
+ ```powershell
50
+ dsh plugin --profile ai-team add @suifei/dsh-ai-team
51
+ ```
52
+
53
+ 或从 checkout 安装:
54
+
55
+ ```powershell
56
+ dsh plugin --profile ai-team add E:/works/dsh-ai-team
57
+ ```
58
+
59
+ ### 3. 启动
60
+
61
+ ```powershell
62
+ dsh --profile ai-team --no-open --port 33271
63
+ ```
64
+
65
+ 打开终端输出的 URL,点侧栏 **文渊阁**。
66
+
67
+ > 插件只应安装在可信机器 / 可信 Workspace:成员以 `danger-full-access` 运行,且 profile patch 会把 sandbox / approval / permission 固定为 `danger-full-access / never`。普通 `desktop`/`web` profile 不安装时保持原有 preset roster。
68
+
69
+ ---
70
+
71
+ ## 界面
72
+
73
+ ```text
74
+ ┌ 文渊阁 ───────────────────────────────────────────────────────────┐
75
+ │ Projects │ Main stream / Board / Situation │ Thread │
76
+ │ # 我的项目 │ 主对话:一句话开工 │ 回复 │
77
+ │ # UI 项目 │ 看板:待处理 / 进行中 / 待验收 / 完成│ 验收 / 退回 │
78
+ │ │ 态势:需要你 / 团队在做 / 等待受阻 / 最近完成 │ 任务动作 │
79
+ │ Agents │ │ │
80
+ │ 沈舟 (Lead) │ │ │
81
+ │ 林岚 (QA) │ │ │
82
+ └─────────────────┴────────────────────────────────────┴─────────────┘
17
83
  ```
18
84
 
19
- The plugin is designed for a dedicated `ai-team` profile; normal `desktop`/`web` sessions keep their preset roster unless explicitly installed. This machine also has it in `desktop` for UI preview. Multi-Project is concurrent: every Project owns its ledger and runtime, each has its own Waker/SSE/tool routing, and `activeProjectId` only selects the UI default. Full install / uninstall / upgrade steps, data locations and profile-isolation checks: [docs/10-安装卸载升级.md](docs/10-安装卸载升级.md).
85
+ - **主对话**:Human 的一句话进入 Main stream,Project owner 拆 Task;Composer 支持讨论 / 任务模式。
86
+ - **看板**:四列;进行中的卡片显示 accountable owner 与执行者;数量只统计 active Task。
87
+ - **态势**:四象限——需要你、团队在做、等待受阻、最近完成。点击条目打开对应 Task 的 Thread。
88
+ - **收件箱**:跨 Project 聚合;失败决策、需要你决定、关注 Task 的完成报告都在这里。
89
+ - **成员资料**:改名、职责、personality、provider/model、运行配方、notes、skills、re-distill、离队/人才库。
90
+
91
+ ---
92
+
93
+ ## 能力地图
94
+
95
+ | 能力 | 说明 |
96
+ |---|---|
97
+ | Project | 具名 owner;并发 runtime;每 Project 独立账本、Waker、SSE |
98
+ | Task | 唯一 accountable owner;pending / in_progress / pending_acceptance / done / cancelled |
99
+ | Thread | append-only;回复带 `baseSeq`,旧基线会被拒并说明变化 |
100
+ | 验收 | maker 不能验收;QA 硬门禁;`qaChecklist` 逐项通过 |
101
+ | 收件箱 | Human/Agent 各自拉取;跨 Project 聚合与项目内 ack |
102
+ | 关注 | 关注 Task;完成后收到 `followed_task_update` |
103
+ | 默认成员 | 新 Project 初始化沈舟 / 林岚 / 顾清;可改名、改职责、改个性、改模型 |
104
+ | 成员 | 持久 session;名字/角色/personality/notes/skills |
105
+ | 模型 | Provider / Model 下拉读取 DSH `llm` 目录;可跟随系统默认 |
106
+ | 进化 | `team.evolve` 只改自己的 personality;验收后 reflection wake |
107
+ | 人才库 | leave/rejoin;遗留 Task 可被接管(taken-over) |
108
+ | 运行配方 | `standard` / `ptc` / `minimal` / `cordis` |
109
+ | 技能 | DSH `skill` 工具真实加载;SkillOpt 7 工具内置 |
110
+ | 重启恢复 | `team/startup` 恢复未完成 Task,无定时器 |
111
+ | 安全 | 每次成员工具调用前守卫 `danger-full-access` |
112
+
113
+ ---
114
+
115
+ ## 架构
116
+
117
+ ```mermaid
118
+ flowchart TB
119
+ H[Human] --> UI[文渊阁 Workbench]
120
+ UI --> HOST[Host /ai-team/api]
121
+ HOST --> R1[ProjectRuntime prj_main]
122
+ HOST --> R2[ProjectRuntime prj_ui]
123
+ R1 --> L1[(append-only ledger)]
124
+ R2 --> L2[(append-only ledger)]
125
+ R1 --> S1[TeamService]
126
+ R2 --> S2[TeamService]
127
+ S1 --> M1[Member sessions]
128
+ S2 --> M2[Member sessions]
129
+ L1 -. Waker / SSE .-> M1
130
+ L2 -. Waker / SSE .-> M2
131
+ ```
132
+
133
+ - **Host**:路由、命令、SSE、Waker、成员 runtime、权限守卫。
134
+ - **ProjectRuntime**:一个 Project 一个 ledger + TeamService;启动时全部打开。
135
+ - **Ledger**:JSONL + SHA-256 hash chain + checkpoint + 纯函数 fold。
136
+ - **MemberRuntime**:真实 DSH Agent session,直接 `agents.create` / `followup`,没有模拟执行。
137
+ - **Waker**:只从账本 append / HTTP 命令结果计划唤醒;没有 heartbeat/timer。
138
+
139
+ ### API 速览
140
+
141
+ ```text
142
+ GET /ai-team/api/health
143
+ GET /ai-team/api/bootstrap?project=<id>
144
+ GET /ai-team/api/projects/<id>/board
145
+ GET /ai-team/api/projects/<id>/quadrants
146
+ GET /ai-team/api/projects/<id>/events # SSE
147
+ POST /ai-team/api/projects/<id>/commands
148
+ ```
149
+
150
+ 命令覆盖 `task.create/claim/submit/accept/request_changes/cancel/reopen/delete/reassign`、`member.add/activate/config.update/profile.update/message/leave/rejoin/takeover`、`project.create/activate`、`inbox.ack` 等。
151
+
152
+ ---
153
+
154
+ ## 技能与 SkillOpt
155
+
156
+ - 成员资料可追加 notes / skills,activation / wake prompt 会注入有界技能目录;
157
+ - 完整技能正文由成员自己的 `skill` 工具加载;
158
+ - `ai-team-files` provider 会读取:
159
+ - `<DSH_HOME>/skills`
160
+ - `<cwd>/.dsh/skills`
161
+ - `<cwd>/.agents/skills`
162
+ - **Microsoft SkillOpt 已内置**:7 个 `skillopt_*` 工具、Python engine bootstrap、`skillopt-sleep` 技能随插件分发,不需要单独安装 `dsh-skillopt`。
163
+
164
+ ---
165
+
166
+ ## 验证
167
+
168
+ - `node --test`:**82 / 82**
169
+ - 文档 QA:`node .ptc/tools/docs_check.mjs` → **0 problems**
170
+ - 真实 E2E 覆盖:
171
+ - 两 Agent claim → Thread → 独立验收;
172
+ - 一句话 → Lead 开卡 → QA 自动验收 → owner 自进化;
173
+ - 强停重启 → `startup_resume` → 继续 Task;
174
+ - 真实浏览器私信成员;
175
+ - 四运行配方分别执行 `run_code` / `pwsh`;
176
+ - 并发 Project + 跨 Project 收件箱;
177
+ - DeepSeek + Zhipu 双订阅 SSE 分桶;
178
+ - V-10 turn 中途权限降级后工具前修复;
179
+ - billing 402 / ECONNRESET 三次重试与恢复;
180
+ - 从 npm 安装后在独立 profile 启动 health 200。
181
+
182
+ 证据目录(本机 `.ptc/rounds/`,不随 npm 发布):
183
+
184
+ ```text
185
+ round-02 两 Agent 独立验收
186
+ round-06 验收后 self-evolve
187
+ round-09 重启 startup_resume
188
+ round-10 资料抽屉私信
189
+ round-11 多 Project 新建/切换
190
+ round-12 成员工具链 + skill 加载
191
+ round-13 四种运行配方
192
+ round-14 安装/卸载 smoke
193
+ round-15 并发/收件箱/V-10/billing
194
+ round-16 四技能安装
195
+ round-17 SkillOpt 内置 + 冷恢复
196
+ round-18 跨 Project 打开 Task + SSE 分桶
197
+ round-19 DeepSeek + Zhipu 双 provider
198
+ ```
199
+
200
+ ---
201
+
202
+ ## 文档
20
203
 
21
- ## Layout
204
+ - [需求研究](docs/01-需求研究报告.md)
205
+ - [PRD](docs/02-产品需求文档-PRD.md)
206
+ - [架构设计](docs/03-架构设计报告.md)
207
+ - [UI/UX/UE 研究](docs/04-UI-UX-UE研究报告.md)
208
+ - [启动与执行计划](docs/05-项目启动与执行计划.md)
209
+ - [会议纪要](docs/06-会议纪要.md)
210
+ - [四象限设计](docs/07-四象限视图研究与设计.md)
211
+ - [异常恢复与模型治理](docs/08-异常恢复与模型治理.md)
212
+ - [团队进化与人才库](docs/09-团队进化与人才库.md)
213
+ - [安装、卸载与升级](docs/10-安装卸载升级.md)
214
+ - [M3 并发多项目与边界补齐](docs/reports/M3-并发多项目与边界补齐.md)
22
215
 
23
- - `index.js` — Host entry: same-origin `/ai-team/api/health`, `/ai-team/api/bootstrap`, and `POST /ai-team/api/projects/:id/commands` backed by the default-project ledger.
24
- - `client.js` — Client entry: `sidebar.panellist` icon + `main` key `ai-team` three-pane workbench.
25
- - `cordis.patch.yml` — bundle row.
26
- - `lib/` — append-only ledger (`ledger/*`), task/thread/inbox domain (`domain/*`) and the host `TeamService` (`host/service.js`).
27
- - `test/` — `node --test`: bundle smoke, ledger/fold/checkpoint, domain rules, and a real-ledger service integration.
216
+ ---
28
217
 
29
- The bundled SkillOpt integration is derived from Microsoft SkillOpt (`plugins/dsh`, MIT); its license is kept at `lib/vendor/skillopt/LICENSE`.
218
+ ## 当前边界
30
219
 
31
- ## Rules
220
+ - 单机、单 Human;不支持多 Human 共享 Team;
221
+ - billing/网络注入是测试 adapter,不是物理断网;
222
+ - SSE 分桶用两个真实 provider 的独立订阅端验证,不是两个浏览器窗口;
223
+ - nuwa / distilly / darwin 不内置,有则用、无则不用;
224
+ - desktop profile 由插件 patch 固定为 `danger-full-access / never`,只应在可信机器使用。
32
225
 
33
- - Only `--dsw-alias-*` tokens for UI chrome; the Loop Mark is the only artwork with its own color.
34
- - No timer / no background wake; M1 will add event-driven wake.
35
- - A′ (native session + tool view) is only the fallback when the workbench is unavailable.
226
+ ---
36
227
 
37
- ## Docs
228
+ ## License
38
229
 
39
- The phase-1 research, PRD, architecture, UX and execution plan live in the repository's `docs/` directory (index: `docs/README.md`).
230
+ MIT。
231
+ Bundled SkillOpt integration derives from [Microsoft SkillOpt](https://github.com/microsoft/SkillOpt) `plugins/dsh` (MIT); its license lives at `lib/vendor/skillopt/LICENSE`.
package/client.js CHANGED
@@ -200,8 +200,14 @@ window.__ModuleLoader__.load({
200
200
  presetPtc: 'PTC 模式',
201
201
  presetMinimal: '极简模式',
202
202
  presetCordis: '创造模式',
203
- memberProvider: 'Provider',
203
+ memberProvider: '模型提供商',
204
204
  memberModel: '模型',
205
+ memberProviderDefault: '跟随系统默认',
206
+ memberModelDefault: '跟随系统默认',
207
+ memberNoProviders: 'DSH 暂未提供可用模型',
208
+ memberModelsLoading: '正在读取 DSH 模型目录…',
209
+ memberModelsError: '无法读取 DSH 模型目录,仍可使用系统默认模型。',
210
+ memberNoModels: '该提供商暂无模型',
205
211
  memberOwner: '设为 Project owner',
206
212
  memberCreate: '创建并激活',
207
213
  memberAdding: '正在创建…',
@@ -468,8 +474,14 @@ window.__ModuleLoader__.load({
468
474
  presetPtc: 'PTC',
469
475
  presetMinimal: 'Minimal',
470
476
  presetCordis: 'Creative',
471
- memberProvider: 'Provider',
477
+ memberProvider: 'Model provider',
472
478
  memberModel: 'Model',
479
+ memberProviderDefault: 'Follow system default',
480
+ memberModelDefault: 'Follow system default',
481
+ memberNoProviders: 'DSH exposes no models yet',
482
+ memberModelsLoading: 'Reading the DSH model directory…',
483
+ memberModelsError: 'Could not read the DSH model directory; system default is still available.',
484
+ memberNoModels: 'No models for this provider',
473
485
  memberOwner: 'Make Project owner',
474
486
  memberCreate: 'Create and activate',
475
487
  memberAdding: 'Creating…',
@@ -1088,6 +1100,8 @@ window.__ModuleLoader__.load({
1088
1100
  .at-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
1089
1101
  .at-field input, .at-field textarea, .at-field select { width: 100%; padding: 6px 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; line-height: 18px; }
1090
1102
  .at-field textarea { min-height: 64px; resize: vertical; }
1103
+ .at-modelfields { display: flex; flex-direction: column; gap: 4px; }
1104
+ .at-fieldhint { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
1091
1105
  .at-fieldrow { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
1092
1106
  .at-modalback { position: fixed; inset: 0; z-index: 80; display: flex; align-items: center; justify-content: center; padding: 20px; background: color-mix(in srgb, var(--dsw-alias-bg-base) 72%, transparent); }
1093
1107
  .at-modal { width: min(420px, 100%); max-height: 86vh; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-2); }
@@ -1842,6 +1856,56 @@ window.__ModuleLoader__.load({
1842
1856
  }
1843
1857
  }
1844
1858
 
1859
+ function ModelFields({ t, catalog, provider, model, onProvider, onModel, error }) {
1860
+ const providers = Array.isArray(catalog?.providers) ? catalog.providers.slice() : [];
1861
+ if (provider !== '' && !providers.some((entry) => entry.id === provider)) {
1862
+ providers.unshift({ id: provider, name: provider, models: model === '' ? [] : [{ id: model, name: model }], modelCount: model === '' ? 0 : 1 });
1863
+ }
1864
+ const selected = providers.find((entry) => entry.id === provider);
1865
+ const models = Array.isArray(selected?.models) ? selected.models.slice() : [];
1866
+ if (model !== '' && !models.some((entry) => entry.id === model)) models.unshift({ id: model, name: model });
1867
+ const fallbackProvider = catalog?.defaultModel?.provider ?? '';
1868
+ const fallbackModel = catalog?.defaultModel?.model ?? '';
1869
+ const chooseProvider = (event) => {
1870
+ const next = event.target.value;
1871
+ onProvider(next);
1872
+ if (next === '') {
1873
+ onModel('');
1874
+ return;
1875
+ }
1876
+ if (fallbackProvider === next && fallbackModel !== '') {
1877
+ onModel(fallbackModel);
1878
+ return;
1879
+ }
1880
+ const entry = providers.find((candidate) => candidate.id === next);
1881
+ onModel(entry?.models?.[0]?.id ?? '');
1882
+ };
1883
+ const hint = error
1884
+ ? error
1885
+ : catalog === null
1886
+ ? t('memberModelsLoading')
1887
+ : providers.length === 0
1888
+ ? t('memberNoProviders')
1889
+ : provider !== '' && models.length === 0
1890
+ ? t('memberNoModels')
1891
+ : '';
1892
+ return h('div', { className: 'at-modelfields' },
1893
+ h('div', { className: 'at-fieldrow' },
1894
+ h('label', { className: 'at-field' }, h('span', null, t('memberProvider')),
1895
+ h('select', { value: provider, onChange: chooseProvider },
1896
+ h('option', { value: '' }, t('memberProviderDefault')),
1897
+ providers.map((entry) => h('option', { key: entry.id, value: entry.id }, entry.name + (entry.modelCount ? ' · ' + entry.modelCount : ''))))),
1898
+ h('label', { className: 'at-field' }, h('span', null, t('memberModel')),
1899
+ h('select', {
1900
+ value: model,
1901
+ disabled: provider === '' || models.length === 0,
1902
+ onChange: (event) => onModel(event.target.value),
1903
+ },
1904
+ h('option', { value: '' }, t('memberModelDefault')),
1905
+ models.map((entry) => h('option', { key: entry.id, value: entry.id }, entry.name === entry.id ? entry.id : entry.name + ' · ' + entry.id))))),
1906
+ hint ? h('div', { className: 'at-fieldhint' }, hint) : null);
1907
+ }
1908
+
1845
1909
  function Workbench({ t, ctx }) {
1846
1910
  const [phase, setPhase] = React.useState('loading');
1847
1911
  const [data, setData] = React.useState(null);
@@ -1870,6 +1934,8 @@ window.__ModuleLoader__.load({
1870
1934
  const [mPrompt, setMPrompt] = React.useState('');
1871
1935
  const [mProvider, setMProvider] = React.useState('');
1872
1936
  const [mModel, setMModel] = React.useState('');
1937
+ const [modelCatalog, setModelCatalog] = React.useState(null);
1938
+ const [modelCatalogError, setModelCatalogError] = React.useState('');
1873
1939
  const [mOwner, setMOwner] = React.useState(false);
1874
1940
  const [mBusy, setMBusy] = React.useState(false);
1875
1941
  const [mError, setMError] = React.useState('');
@@ -1949,6 +2015,28 @@ window.__ModuleLoader__.load({
1949
2015
  };
1950
2016
  }, [projectId]);
1951
2017
 
2018
+ React.useEffect(() => {
2019
+ let alive = true;
2020
+ fetch('/ai-team/api/models', { headers: { accept: 'application/json' } })
2021
+ .then(async (response) => {
2022
+ const body = await response.json().catch(() => null);
2023
+ if (!response.ok) throw new Error(body?.error?.message ?? ('HTTP ' + response.status));
2024
+ return body;
2025
+ })
2026
+ .then((body) => {
2027
+ if (!alive) return;
2028
+ setModelCatalog({
2029
+ defaultModel: body?.defaultModel ?? null,
2030
+ providers: Array.isArray(body?.providers) ? body.providers : [],
2031
+ });
2032
+ if (body?.ok === false) setModelCatalogError(body?.error?.message ?? t('memberModelsError'));
2033
+ })
2034
+ .catch((error) => {
2035
+ if (alive) setModelCatalogError(error?.message ?? t('memberModelsError'));
2036
+ });
2037
+ return () => { alive = false; };
2038
+ }, []);
2039
+
1952
2040
  React.useEffect(() => {
1953
2041
  if (!data) return;
1954
2042
  if (!data.projects.some((project) => project.id === projectId)) {
@@ -2175,7 +2263,7 @@ window.__ModuleLoader__.load({
2175
2263
  return body;
2176
2264
  }
2177
2265
  function openAddAgent() {
2178
- const defaults = data?.runtime?.defaultModel ?? {};
2266
+ const defaults = modelCatalog?.defaultModel ?? data?.runtime?.defaultModel ?? {};
2179
2267
  setMName('');
2180
2268
  setMRole('');
2181
2269
  setMPrompt('');
@@ -2780,11 +2868,15 @@ window.__ModuleLoader__.load({
2780
2868
  onChange: (event) => setMPrompt(event.target.value),
2781
2869
  placeholder: t('memberPromptHint'),
2782
2870
  })),
2783
- h('div', { className: 'at-fieldrow' },
2784
- h('label', { className: 'at-field' }, h('span', null, t('memberProvider')),
2785
- h('input', { value: mProvider, onChange: (event) => setMProvider(event.target.value), placeholder: 'deepseek-account' })),
2786
- h('label', { className: 'at-field' }, h('span', null, t('memberModel')),
2787
- h('input', { value: mModel, onChange: (event) => setMModel(event.target.value), placeholder: 'deepseek-flash' }))),
2871
+ h(ModelFields, {
2872
+ t,
2873
+ catalog: modelCatalog,
2874
+ provider: mProvider,
2875
+ model: mModel,
2876
+ onProvider: setMProvider,
2877
+ onModel: setMModel,
2878
+ error: modelCatalogError,
2879
+ }),
2788
2880
  h('label', { className: 'at-check' },
2789
2881
  h('input', { type: 'checkbox', checked: mOwner, onChange: (event) => setMOwner(event.target.checked) }),
2790
2882
  h('span', null, t('memberOwner'))),
@@ -2826,12 +2918,17 @@ window.__ModuleLoader__.load({
2826
2918
  h('option', { value: 'ptc' }, t('presetPtc')),
2827
2919
  h('option', { value: 'minimal' }, t('presetMinimal')),
2828
2920
  h('option', { value: 'cordis' }, t('presetCordis')))),
2829
- h('label', { className: 'at-field' }, h('span', null, t('memberProvider')),
2830
- h('input', { value: pProvider, onChange: (event) => setPProvider(event.target.value), placeholder: 'deepseek-account' })),
2831
- h('label', { className: 'at-field' }, h('span', null, t('memberModel')),
2832
- h('input', { value: pModel, onChange: (event) => setPModel(event.target.value), placeholder: 'deepseek-flash' })),
2833
2921
  h('label', { className: 'at-field' }, h('span', null, t('memberMaxTokens')),
2834
2922
  h('input', { value: pMaxTokens, onChange: (event) => setPMaxTokens(event.target.value), inputMode: 'numeric' }))),
2923
+ h(ModelFields, {
2924
+ t,
2925
+ catalog: modelCatalog,
2926
+ provider: pProvider,
2927
+ model: pModel,
2928
+ onProvider: setPProvider,
2929
+ onModel: setPModel,
2930
+ error: modelCatalogError,
2931
+ }),
2835
2932
  h('label', { className: 'at-field' }, h('span', null, t('successor')),
2836
2933
  h('select', {
2837
2934
  value: pSuccessor,
package/index.js CHANGED
@@ -24,6 +24,7 @@ import { installFileSkillProvider } from './lib/runtime/skill-provider.js';
24
24
  import { apply as applySkillOpt } from './lib/vendor/skillopt/src/index.js';
25
25
  import { createMemberPermissionGuard } from './lib/host/permission-guard.js';
26
26
  import { installFaultAdapter } from './lib/host/fault-adapter.js';
27
+ import { buildModelCatalog } from './lib/host/model-catalog.js';
27
28
  import { createTeamTool } from './lib/host/member-tools.js';
28
29
  import { describeWake, planWakes } from './lib/host/waker.js';
29
30
 
@@ -32,7 +33,7 @@ export const name = 'ai-team';
32
33
  /** The workbench only makes sense where the Web carrier exists. */
33
34
  export const inject = ['webServer', 'permissionPresets', 'sandboxPolicy'];
34
35
 
35
- const VERSION = '0.3.3';
36
+ const VERSION = '0.3.5';
36
37
  const PROJECT_ID = 'prj_main';
37
38
  const HUMAN_ID = 'mem_human';
38
39
 
@@ -592,6 +593,10 @@ export function apply(ctx) {
592
593
  get: await skillsService.get('distilly', {}).then((skill) => skill?.name ?? null).catch(() => null),
593
594
  });
594
595
  }
596
+ if (req.method === 'GET' && url.pathname === '/ai-team/api/models') {
597
+ const catalog = await buildModelCatalog(ctx.get('llm'), ctx.get('agentDefaultModel'));
598
+ return send(res, catalog);
599
+ }
595
600
  if (req.method === 'GET' && url.pathname === '/ai-team/api/health') {
596
601
  const head = booted.ledger.head;
597
602
  return send(res, {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The initial 文渊阁 roster. These are real Members in the Project ledger, not
3
+ * UI placeholders: the Human can rename/re-role/re-model them, and the Lead is
4
+ * the default Project owner so a one-line Main-stream request can start work.
5
+ *
6
+ * Seeded only when a Project has no Agent member yet. Once a member exists the
7
+ * user's roster is authoritative and boot never adds these people back.
8
+ */
9
+
10
+ export const DEFAULT_MEMBER_ROSTER = Object.freeze([
11
+ {
12
+ id: 'mem_lead',
13
+ name: '沈舟',
14
+ role: '主理人',
15
+ personality: '先定义可验收的结果,再把方向拆成 Task;不替执行者写实现,只在关键时刻做决定。',
16
+ },
17
+ {
18
+ id: 'mem_maker',
19
+ name: '林岚',
20
+ role: '执行',
21
+ personality: '认领 Task 后直接推进,先交付可运行结果和证据再汇报;遇到阻塞先找 owner。',
22
+ },
23
+ {
24
+ id: 'mem_check',
25
+ name: '顾清',
26
+ role: '验收',
27
+ personality: '独立验收,只按结果定义和证据判断;不确定就退回,不与执行者共用 claim。',
28
+ },
29
+ ]);
30
+
31
+ export const DEFAULT_LEAD_ID = 'mem_lead';
32
+
33
+ function agentMembers(service) {
34
+ return Object.values(service.state.members ?? {}).filter((member) => member.kind === 'agent');
35
+ }
36
+
37
+ /**
38
+ * Add the default roster to an empty Project and make the Lead its owner.
39
+ * @returns {Promise<{seeded: boolean, members: string[]}>}
40
+ */
41
+ export async function seedDefaultRoster(service, projectId, { humanId = 'mem_human' } = {}) {
42
+ if (agentMembers(service).length > 0) return { seeded: false, members: [] };
43
+ const added = [];
44
+ for (const member of DEFAULT_MEMBER_ROSTER) {
45
+ await service.dispatch({
46
+ type: 'member.add',
47
+ actor: { kind: 'human', memberId: humanId },
48
+ projectId,
49
+ payload: {
50
+ id: member.id,
51
+ name: member.name,
52
+ role: member.role,
53
+ personality: member.personality,
54
+ kind: 'agent',
55
+ status: 'idle',
56
+ },
57
+ });
58
+ added.push(member.id);
59
+ }
60
+ const project = service.state.projects?.[projectId];
61
+ const owner = project?.ownerMemberId ?? project?.ownerId;
62
+ if (owner === humanId || owner === undefined || owner === null) {
63
+ await service.dispatch({
64
+ type: 'project.owner.update',
65
+ actor: { kind: 'human', memberId: humanId },
66
+ projectId,
67
+ payload: { ownerMemberId: DEFAULT_LEAD_ID, reason: 'seed default roster' },
68
+ });
69
+ }
70
+ return { seeded: true, members: added };
71
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Read the model directory from the live DSH `llm` service. The workbench must
3
+ * never ask the user to type provider/model ids: those ids already exist in the
4
+ * Host and can change when accounts/adapters are configured.
5
+ */
6
+
7
+ function safeDefault(agentDefaultModel) {
8
+ try {
9
+ const selection = agentDefaultModel?.currentSelection?.();
10
+ if (selection === null || typeof selection !== 'object') return null;
11
+ if (typeof selection.provider !== 'string' || typeof selection.model !== 'string') return null;
12
+ return { provider: selection.provider, model: selection.model };
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ function modelRows(models) {
19
+ const seen = new Set();
20
+ const rows = [];
21
+ for (const model of Array.isArray(models) ? models : []) {
22
+ if (model === null || typeof model !== 'object' || typeof model.id !== 'string' || model.id === '') continue;
23
+ if (seen.has(model.id)) continue;
24
+ seen.add(model.id);
25
+ rows.push({
26
+ id: model.id,
27
+ name: typeof model.name === 'string' && model.name !== '' ? model.name : model.id,
28
+ ...(typeof model.description === 'string' && model.description !== '' ? { description: model.description } : {}),
29
+ ...(Array.isArray(model.inputModalities) ? { inputModalities: model.inputModalities } : {}),
30
+ });
31
+ }
32
+ return rows;
33
+ }
34
+
35
+ /**
36
+ * @param {any} llm live DSH llm service
37
+ * @param {any} agentDefaultModel live DSH default-model service
38
+ */
39
+ export async function buildModelCatalog(llm, agentDefaultModel) {
40
+ const defaultModel = safeDefault(agentDefaultModel);
41
+ if (llm === undefined || llm === null || typeof llm.listProviders !== 'function') {
42
+ return {
43
+ ok: false,
44
+ defaultModel,
45
+ providers: [],
46
+ error: { code: 'E_RUNTIME_UNAVAILABLE', message: 'the DSH llm service is unavailable' },
47
+ };
48
+ }
49
+ let providers;
50
+ try {
51
+ providers = llm.listProviders();
52
+ } catch (error) {
53
+ return {
54
+ ok: false,
55
+ defaultModel,
56
+ providers: [],
57
+ error: { code: 'E_MODEL_DIRECTORY', message: error?.message ?? String(error) },
58
+ };
59
+ }
60
+ const rows = await Promise.all((Array.isArray(providers) ? providers : []).map(async (provider) => {
61
+ const id = typeof provider?.id === 'string' ? provider.id : '';
62
+ const name = typeof provider?.name === 'string' && provider.name !== '' ? provider.name : id;
63
+ if (id === '') return null;
64
+ try {
65
+ const models = modelRows(await llm.listModels(id));
66
+ return { id, name, models, modelCount: models.length };
67
+ } catch (error) {
68
+ return {
69
+ id,
70
+ name,
71
+ models: [],
72
+ modelCount: 0,
73
+ error: error?.message ?? String(error),
74
+ };
75
+ }
76
+ }));
77
+ const available = rows.filter((row) => row !== null);
78
+ return {
79
+ ok: true,
80
+ defaultModel,
81
+ providers: available,
82
+ providerCount: available.length,
83
+ modelCount: available.reduce((total, row) => total + row.models.length, 0),
84
+ };
85
+ }
@@ -10,6 +10,7 @@
10
10
  import { mkdirSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { openLedger } from '../ledger/writer.js';
13
+ import { seedDefaultRoster } from './default-roster.js';
13
14
  import { TeamService } from './service.js';
14
15
 
15
16
  export const DEFAULT_PROJECT_ID = 'prj_main';
@@ -59,6 +60,7 @@ export class ProjectRuntime {
59
60
  payload: { id: projectId, name: projectName, goal: projectGoal },
60
61
  });
61
62
  }
63
+ await seedDefaultRoster(service, projectId, { humanId: HUMAN_ID });
62
64
  }
63
65
  return runtime;
64
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suifei/dsh-ai-team",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "description": "文渊阁 (Wenyuan Pavilion) — AI Team mode for DeepSeek Harness: concurrent projects, owned tasks, threads, inbox and an append-only team ledger",
6
6
  "license": "MIT",