@tanweilabs/daiti 0.0.2 → 0.0.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/README.md +20 -4
- package/artifacts/manifest.json +12 -12
- package/artifacts/{tanweilabs-daiti-im-0.0.2-16f94aa853b3.tgz → tanweilabs-daiti-im-0.0.5-840b72814195.tgz} +0 -0
- package/artifacts/tanweilabs-daiti-loading-0.0.5-25d3a3be10fb.tgz +0 -0
- package/artifacts/tanweilabs-daiti-preferences-0.0.5-501242b3c269.tgz +0 -0
- package/artifacts/tanweilabs-daiti-ui-0.0.5-35ca52ba5687.tgz +0 -0
- package/docs/releasing.md +68 -0
- package/docs/validation.md +40 -1
- package/lib/cli.mjs +127 -6
- package/lib/supervisor.mjs +3 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/artifacts/tanweilabs-daiti-loading-0.0.2-fc449733ecaa.tgz +0 -0
- package/artifacts/tanweilabs-daiti-preferences-0.0.2-a0b22ddd8594.tgz +0 -0
- package/artifacts/tanweilabs-daiti-ui-0.0.2-3c9ee486f937.tgz +0 -0
package/README.md
CHANGED
|
@@ -2,19 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
在你的电脑上运行,通过网页或微信使用的个人助理。基于 DeepSeek Harness 和 DSH-IM,通过独立插件提供单主会话、助理设置和首次使用流程。
|
|
4
4
|
|
|
5
|
-
**0.0.
|
|
5
|
+
**0.0.5 为开发者预览版。** 本包不包含个人记忆、历史会话、密钥或 IM 登录数据。
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
9
9
|
需要 Node.js `^22.19.0` 或 `>=24.0.0`:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npm install -g @tanweilabs/daiti@0.0.
|
|
12
|
+
npm install -g @tanweilabs/daiti@0.0.5
|
|
13
13
|
daiti start
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
启动后自动打开本机网页。可用 `daiti status`、`daiti open` 和 `daiti stop` 管理服务。
|
|
17
17
|
|
|
18
|
+
## 查看版本与升级
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
daiti version # 当前版本;有新版本时会提示
|
|
22
|
+
daiti update --check # 只检查
|
|
23
|
+
daiti update # 安装最新版并自动重启服务(服务原本在跑才重启)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
设置页"助理"下方也会显示当前版本。`start`/`status` 同样会打印版本;`daiti update` 会写进 `service.log`。
|
|
27
|
+
|
|
18
28
|
## 从源码体验
|
|
19
29
|
|
|
20
30
|
安装 Node.js `^22.19.0` 或 `>=24.0.0`,然后在仓库根目录运行:
|
|
@@ -63,7 +73,11 @@ PowerShell 对应设置 `$env:DAITI_HOME = "$HOME\.daiti-preview"`。测试时
|
|
|
63
73
|
|
|
64
74
|
适配包固定 DSH-IM 4.21.0,构建时复用其源码和运行时构造扩展点;不修改原包文件,也不修改 Harness 核心。这些扩展点不是稳定公开 API,上游升级需要契约测试。原许可证保存在适配包中。
|
|
65
75
|
|
|
66
|
-
0.0.2 已验证主会话绑定与连接健康状态,微信与网页共用会话已由用户实际验收。
|
|
76
|
+
0.0.2 已验证主会话绑定与连接健康状态,微信与网页共用会话已由用户实际验收。0.0.3 在此基础上修复了新装实例的插件层顺序问题(此前全新 `DAITI_HOME` 首次启动会因 `dshIm` 重复注册而失败),并默认启用实验性的 Agent Team 层。
|
|
77
|
+
|
|
78
|
+
## Agent Team
|
|
79
|
+
|
|
80
|
+
0.0.3 起,profile 默认挂载 `@deepseek-ai/dsh-experimental-agent-team-profile` 与 `-web-profile`:主会话可以作为 Team Lead 用 `spawn_teammate` 建立具名成员,成员是有独立会话的 durable 子代理,通过共享任务板与 `send_message`/`wait_agent` 协作;浏览器侧由官方 Agent Team 面板展示名册与任务板。该层为官方实验特性,不承诺稳定;启用后 Harness 的全局 `subagent`/`subagent_fork` 工具被 Team 工具替代(`workflow` 不受影响),而"标准模式"预设内的子代理工具是否同时生效仍待真实会话验收。
|
|
67
81
|
|
|
68
82
|
## 开发和本地打包
|
|
69
83
|
|
|
@@ -75,7 +89,7 @@ npm run pack:local
|
|
|
75
89
|
|
|
76
90
|
构建将四个插件打包到 `artifacts/`,生成文件名和 SHA-256 清单。`pack:local` 在 `.local/releases/` 生成 CLI 安装包;包含运行入口、插件包和文档,不包含仓库工作目录、上游源码、个人配置或测试数据。
|
|
77
91
|
|
|
78
|
-
未发布的本地安装包可用 `npm install -g /path/to/tanweilabs-daiti-0.0.
|
|
92
|
+
未发布的本地安装包可用 `npm install -g /path/to/tanweilabs-daiti-0.0.5.tgz` 安装,随后使用 `daiti start`。安装仍需联网获取声明的运行依赖。
|
|
79
93
|
|
|
80
94
|
- `bin/`、`lib/`:CLI 和后台进程管理。
|
|
81
95
|
- `packages/assistant-ui/`:界面、首次设置和助理配置。
|
|
@@ -94,3 +108,5 @@ npm run pack:local
|
|
|
94
108
|
## 许可证
|
|
95
109
|
|
|
96
110
|
代体代码采用 MIT;依赖保留各自许可证。见 [LICENSE](LICENSE) 和 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。本项目不是 DeepSeek 官方客户端。
|
|
111
|
+
|
|
112
|
+
发布维护步骤见 [发布指南](docs/releasing.md)。
|
package/artifacts/manifest.json
CHANGED
|
@@ -2,27 +2,27 @@
|
|
|
2
2
|
"plugins": [
|
|
3
3
|
{
|
|
4
4
|
"name": "@tanweilabs/daiti-ui",
|
|
5
|
-
"version": "0.0.
|
|
6
|
-
"file": "tanweilabs-daiti-ui-0.0.
|
|
7
|
-
"sha256": "
|
|
5
|
+
"version": "0.0.5",
|
|
6
|
+
"file": "tanweilabs-daiti-ui-0.0.5-35ca52ba5687.tgz",
|
|
7
|
+
"sha256": "35ca52ba5687592caeafc3a0e879b1ee0d95f89ec5c32458d2204eae46165331"
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"name": "@tanweilabs/daiti-preferences",
|
|
11
|
-
"version": "0.0.
|
|
12
|
-
"file": "tanweilabs-daiti-preferences-0.0.
|
|
13
|
-
"sha256": "
|
|
11
|
+
"version": "0.0.5",
|
|
12
|
+
"file": "tanweilabs-daiti-preferences-0.0.5-501242b3c269.tgz",
|
|
13
|
+
"sha256": "501242b3c2693dc3787ed0907a8f2ab8c24e5fcd9519f1ad91b637e7477859cb"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"name": "@tanweilabs/daiti-loading",
|
|
17
|
-
"version": "0.0.
|
|
18
|
-
"file": "tanweilabs-daiti-loading-0.0.
|
|
19
|
-
"sha256": "
|
|
17
|
+
"version": "0.0.5",
|
|
18
|
+
"file": "tanweilabs-daiti-loading-0.0.5-25d3a3be10fb.tgz",
|
|
19
|
+
"sha256": "25d3a3be10fb4ecbaa5f84083fa557ff6886986cea1a96909db607bb543f1a94"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"name": "@tanweilabs/daiti-im",
|
|
23
|
-
"version": "0.0.
|
|
24
|
-
"file": "tanweilabs-daiti-im-0.0.
|
|
25
|
-
"sha256": "
|
|
23
|
+
"version": "0.0.5",
|
|
24
|
+
"file": "tanweilabs-daiti-im-0.0.5-840b72814195.tgz",
|
|
25
|
+
"sha256": "840b72814195ccf1f3c65283265b8fb8794441ae0d396cb005a68ae7b55f3e99"
|
|
26
26
|
}
|
|
27
27
|
]
|
|
28
28
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# npm 发布指南(供维护者与 AI 使用)
|
|
2
|
+
|
|
3
|
+
包名为 `@tanweilabs/daiti`,发布到官方 registry;GitHub 仓库为 `tanweilabs/daiti`。Git 提交、Git 标签和 npm 发布是三件独立的事。只有 Git 标签不表示 npm 已发布;本机安装的版本也不证明 npm 上存在。
|
|
4
|
+
|
|
5
|
+
发布必须有用户明确授权。不得为了绕过二次验证而降低账户安全配置。以下以 0.0.5 为例,后续替换版本号;已经发布的版本不可覆盖。
|
|
6
|
+
|
|
7
|
+
## 1. 检查与同步版本
|
|
8
|
+
|
|
9
|
+
在产品仓库根目录执行:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
git status --short
|
|
13
|
+
npm whoami
|
|
14
|
+
npm view @tanweilabs/daiti versions dist-tags --json --registry https://registry.npmjs.org/
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
保留他人未提交改动,不使用 reset/clean。同步根 `package.json`、`packages/*/package.json`、`npm-shrinkwrap.json` 的顶层与 `packages[""].version`、README 安装示例。不要改动依赖的版本来凑产品版本。
|
|
18
|
+
|
|
19
|
+
## 2. 验证、构建、检查实际安装包
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm test
|
|
23
|
+
npm run build
|
|
24
|
+
mkdir -p .local/releases
|
|
25
|
+
npm pack --json --pack-destination .local/releases
|
|
26
|
+
tar -tzf .local/releases/tanweilabs-daiti-0.0.5.tgz
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
检查外层文件清单及 `artifacts/` 内的四个插件包:版本一致,许可证保留,没有个人工作目录、聊天、密钥、登录数据、日志或 `.local/` 文件。运行逻辑有改动时,用独立 `DAITI_HOME` 验证启动/状态/停止、重复启动及重启后主会话不变,不接入用户的真实微信账号。不要将服务 ready 等同于真实消息收发验收。
|
|
30
|
+
|
|
31
|
+
发布的是检查过的 `.tgz`,不是另一次临时生成的目录包。若修改了任何发布文件,重新构建和打包。
|
|
32
|
+
|
|
33
|
+
## 3. 提交与标签
|
|
34
|
+
|
|
35
|
+
在用户授权范围内,逐项检查并暂存此次发布文件,提交、推送。创建 `v0.0.5` 标签并推送;若标签已存在,核对其提交,不强行移动或覆盖。提交 ID 和实际安装包摘要保留在发布结果中。
|
|
36
|
+
|
|
37
|
+
## 4. npm 发布:必须保留交互终端
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm publish .local/releases/tanweilabs-daiti-0.0.5.tgz --access public --registry https://registry.npmjs.org/ --auth-type=web
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
AI 使用终端工具时设置 **`tty: true`**。保留返回的终端 `session_id`,不要关闭等待中的进程。非交互发布可能直接报 `EOTP`,即 npm 要求本次发布的二次验证,不代表包有问题或未登录。
|
|
44
|
+
|
|
45
|
+
终端显示 `https://www.npmjs.com/auth/cli/...` 后,按回车打开浏览器;若页面未打开,把本次链接打开并交给用户完成验证。每次使用新链接,不复制旧发布的认证链接。登录状态不代表以后每次发布都可免二次验证。不向用户索取或记录长期 Token、OTP、认证链接到仓库。
|
|
46
|
+
|
|
47
|
+
用户说验证完成后,继续读取**原终端会话**的输出,不要立即重新执行 publish。若会话失效,先查询 registry 和日志确认是否已接受发布,避免重复发布。
|
|
48
|
+
|
|
49
|
+
## 5. 分清“接受发布”和“可下载”
|
|
50
|
+
|
|
51
|
+
`+ @tanweilabs/daiti@0.0.5` 表示 npm 接受了发布;若同时显示 processing,仍可能暂时查询不到。此时不能立刻声称用户已可安装,也不要反复发布。
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npm view @tanweilabs/daiti@0.0.5 version dist.shasum --json --registry https://registry.npmjs.org/
|
|
55
|
+
npm view @tanweilabs/daiti dist-tags --json --registry https://registry.npmjs.org/
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
确认版本可查询、`dist.shasum` 与 `npm pack --json` 的 shasum 一致,以及 `latest` 指向目标版本。若具体版本已可查询但标签仍旧,先排除短暂同步延迟;确需修正且用户授权默认发布时,可执行 `npm dist-tag add @tanweilabs/daiti@0.0.5 latest --registry https://registry.npmjs.org/`(也可能触发验证)。遇到 E403/权限错误先核对账号与组织权限,不能把所有错误都归因于 EOTP。
|
|
59
|
+
|
|
60
|
+
## 6. 用户升级
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
daiti stop
|
|
64
|
+
npm install -g @tanweilabs/daiti@0.0.5
|
|
65
|
+
daiti start
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
在独立终端执行,不能让代体在自己的运行会话里关闭自身后再负责启动。升级不应更换 `DAITI_HOME`;保留现有数据并先备份。其他浏览器提示无权限时运行 `daiti open` 打开认证入口。
|
package/docs/validation.md
CHANGED
|
@@ -27,10 +27,49 @@
|
|
|
27
27
|
|
|
28
28
|
18项自动测试通过,其中9项为IM路由、本人私聊限制、缺主会话拒绝、目录同一性及真实上游 ask 路径。隔离 Host probe 已通过实际 modernHarnessApi、HarnessClient 和 BotWorkspaceStore 的绑定及持久化回读。真实账号启动时发现大小写不同但指向同目录的路径误判,改为文件系统身份比较并增加回归测试。修复后账号显示1/1在线,状态healthy且error为空,绑定ID与网页主ID一致;旧独立微信会话文件哈希仍与备份一致。未主动发送真实微信消息;跨端模型回复待用户验证。本地修复未发布npm。
|
|
29
29
|
|
|
30
|
-
## 2026-09-21
|
|
30
|
+
## 2026-09-21 自动端口选择(随 0.0.2 发布)
|
|
31
31
|
|
|
32
32
|
- 默认启动先尝试 3086;被占用时由操作系统分配空闲本机端口,实际端口用于就绪校验、状态查询和浏览器地址。显式 `--port` 冲突时明确报错。
|
|
33
33
|
- 同一 DAITI_HOME 的已认证服务优先复用,不因端口参数重复启动。
|
|
34
34
|
- `npm test` 20 项通过,`npm run build` 通过。
|
|
35
35
|
- 独立 `.local/acceptance` 实例真实启动:3086 已占用时选择 55957,重复启动保持同一实例;停止后显式 3086 报错且未创建服务;再次默认启动选择 56049,主会话 ID 不变;最后测试服务已停止,个人 3086 服务仍就绪。
|
|
36
36
|
- 端口探测与 Harness 绑定之间存在很短的竞争窗口;本次没有覆盖其他进程恰在该窗口抢占新端口的情形。
|
|
37
|
+
|
|
38
|
+
## 2026-09-21 浏览器标签页标题(0.0.3)
|
|
39
|
+
|
|
40
|
+
- 背景:Harness 在客户端构建时写死 `DSH_CLIENT_TITLE`,标签页显示为 `会话标题 — DeepSeek Harness`;打包成个人助理后应以产品名呈现。
|
|
41
|
+
- 改动:`packages/assistant-ui/document-title.js` 在插件装载期间接管 `document.title`,忽略后续写入并在卸载时释放;文案常量为 `代体`,可传 `title` 覆盖。
|
|
42
|
+
- 未改上游文件,也未重新构建 Harness 前端;产品名仍来自官方构建产物,仅在运行时被接管。
|
|
43
|
+
- `npm test` 25 项通过(含 5 项标题接管:接管、忽略后续写入、卸载释放、自定义/空文案、无 document 时不崩);`npm run build` 通过,`@tanweilabs/daiti-ui` 升到 0.0.3。
|
|
44
|
+
- 已确认:`.local/acceptance` 实例重新安装后,profile 内 `client.cjs` 含接管代码;个人 3086 服务未被改动。
|
|
45
|
+
- 未验证:没有在真实浏览器里确认标签页文字与首屏闪烁;接管发生在客户端插件加载后,Harness 自带 title 在此之前可能短暂可见。
|
|
46
|
+
|
|
47
|
+
## 2026-09-21 忙碌时默认插话(0.0.3,未发布)
|
|
48
|
+
|
|
49
|
+
- 背景:Harness `ui-conversation` 的 `busyEnter` 默认是 `queue`(助理忙碌时回车排队),代体要改成忙碌时插话(`steer`)。上游注册该命名空间时不传 composition `base`、也不读入口 config,所以 profile patch 层无法直接改这个默认值。
|
|
50
|
+
- 改动:`packages/assistant-ui/busy-enter-default.js` 在插件启动时通过官方 settings 服务(`ctx.settings.describe()` / `update()`)把 `ui-conversation.busyEnter` 写成 `steer`;字段已存在于命名空间的用户层(用户在设置页选过)时不覆盖,因此重启幂等、用户显式选择优先。`onboarding-host.js` 调用安装,`package.json` 的 `files` 增加该模块。
|
|
51
|
+
- 未改上游文件与 `node_modules`,未新增依赖;写的是官方用户设置文档 `$DSH_HOME/settings.yaml`(即设置页/模型页写的那份),YAML 格式、注释保留、revision 与校验仍由上游负责。
|
|
52
|
+
- `npm test` 36 项通过(新增 11 项:字段与取值、无用户选择时写入、显式选择保留、已是 steer 不重写、等命名空间注册、写失败重试与放弃、dispose 后停止、无 settings 服务不崩、`apply` 接线);`npm run build` 通过,4 个插件包。`@tanweilabs/daiti-ui` 维持 0.0.3(0.0.3 尚未发布,与标签页标题同批)。
|
|
53
|
+
- 独立 `DAITI_HOME` 验收(`.local/acceptance`,已有 onboarding 数据但无 settings 文档):启动后生成 `runtime/settings.yaml`,内容 `ui-conversation:` / ` busyEnter: steer`;重启后该文件哈希与 mtime 均未变、主会话 ID 不变;手工改成 `queue` 再启动仍是 `queue`(用户选择未被覆盖);测试服务已停止,个人 3086 未被改动。
|
|
54
|
+
- 隔离 probe(`.local/busy-enter-probe.mjs`,一次性脚本,未随包发布)用真实 `@deepseek-ai/dsh-settings-file` 与真实 `ui-conversation` host 装载验收实例的 `runtime/`:`ctx.settings.get('ui-conversation')` 为 `{busyEnter:'steer'}`,`describe()` 中 `value` 与 `user` 都是 steer。
|
|
55
|
+
- 未验证:没有在真实浏览器与真实模型回合里确认忙碌时回车的实际投递;上游 `resolveSubmitMode('steer', true, 'enter', true) === 'steer'` 目前只是源码阅读结论。
|
|
56
|
+
- 另发现(已在本版修复,见下节):全新空 `DAITI_HOME` 的首次安装启动失败,`service "dshIm" has been registered at <dsh-im-host>`;当前工作区和已打包的 `@tanweilabs/daiti@0.0.2` 都能复现,原因是 profile 的 `dsh.profile.bundles` 中 `@tanweilabs/daiti-im` 排在 `@xmanrui/dsh-im` 之前。手工把 `@xmanrui/dsh-im` 提前后同一目录可正常启动,本功能的验收就是在该顺序下完成的。
|
|
57
|
+
|
|
58
|
+
## 2026-09-21 新装启动修复与 Agent Team 开启(0.0.3)
|
|
59
|
+
|
|
60
|
+
- 修复新装崩溃:`lib/cli.mjs` 的 `setup()` 现在把 profile 的 `dsh.profile.bundles` 规范成固定顺序(`dsh-base`、`dsh-web-app`、`@xmanrui/dsh-im`、Agent Team 两层、`daiti-loading`、`daiti-preferences`、`daiti-ui`、`daiti-im` 最后)。两个顺序是承重的:上游 IM 必须在适配器之前,Team 层必须在 UI 层之前。未知 bundle 保留原相对位置并排在已知层之后;必需的 IM bundle 解析不到时直接报错,不再静默排出会导致重复注册的顺序。
|
|
61
|
+
- 开启 Agent Team:`setup()` 把 `@deepseek-ai/dsh-experimental-agent-team-profile` 与 `-web-profile` 写进 profile 依赖(版本取自本包依赖树的 `0.1.6-alpha.2`)并执行 `pnpm install --ignore-scripts`,再按上述顺序加入 `bundles`;解析不到时告警并跳过,不阻塞安装。
|
|
62
|
+
- `npm test` 37 项通过(新增 1 项覆盖 bundle 顺序、未知 bundle 保留与必需 bundle 缺失报错);`npm run build` 通过,4 个插件包均为 0.0.3。
|
|
63
|
+
- 全新空 `DAITI_HOME` 端到端验收(`.local/fresh-003`,端口 3095):首次安装完成、服务就绪,`service.log` 无 `has been registered` 失败;profile 顺序为 `dsh-base → dsh-web-app → @xmanrui/dsh-im → agent-team-profile → agent-team-web-profile → daiti-ui → daiti-im`;5 个 `@deepseek-ai/dsh-experimental-*` 包已装入 profile 的 `node_modules`。
|
|
64
|
+
- `--dump-config` 确认:base 的四个子代理工具行已 `disabled: true`,Team 层 `agent-team` 与 `tool-agent-team` 行已插入。
|
|
65
|
+
- 未验证(发布前已知):真实的浏览器会话里 Team 工具是否按预期出现;"标准模式"预设内自带的子代理工具行不受 profile patch 影响,可能与 Team 的 `list_agents`/`send_message` 同名共存(参数不同),需要真实会话验收;Team 策略会引导 Lead 使用 `wait_agent`(10s–1h),是否加重主会话阻塞待观察。
|
|
66
|
+
|
|
67
|
+
## 2026-09-21 自动端口选择(0.0.2)
|
|
68
|
+
|
|
69
|
+
## 2026-09-21 版本可见与自助升级(0.0.4)
|
|
70
|
+
|
|
71
|
+
- 新增 `daiti version`:打印当前版本,并从 npm 查询最新版本,有新版本时提示运行 `daiti update`;查不到网络时明确说明而不是静默。
|
|
72
|
+
- 新增 `daiti update [--check]`:`--check` 只比对;不带参数时 `npm install -g @tanweilabs/daiti@<latest>`,若服务原本在跑则先 stop、再用**全局安装的** `bin/daiti.mjs` 拉起新版本,最后等就绪并打印端口;原本没跑就只装不启动。安装失败或服务未就绪都会抛错,不静默当作成功。
|
|
73
|
+
- `start`/`status` 现在打印版本行,并把带时间戳的版本记录写进 `service.log`;设置页「助理」下方显示 `代体 v<版本>` 与升级命令。
|
|
74
|
+
- `npm test` 39 项通过(新增 2 项:版本读取与版本比较、`update/version` 参数校验;原有参数断言补 `check: false`);`npm run build` 通过,四个插件包升到 0.0.4。
|
|
75
|
+
- 未验证:`daiti update` 的**真实升级路径**(需要有比当前更高的已发布版本才能端到端验证)、升级中断电或网络失败时的恢复、Windows 下的 `npm root -g` 路径。
|
package/lib/cli.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
2
|
+
import { spawn, execFile } from 'node:child_process';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
8
9
|
import { selectPort } from './ports.mjs';
|
|
9
10
|
export const root = fileURLToPath(new URL('../', import.meta.url));
|
|
10
11
|
export const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -19,14 +20,16 @@ export async function atomicJson(file, value) {
|
|
|
19
20
|
await fs.rename(temp, file);
|
|
20
21
|
}
|
|
21
22
|
export function parseArgs(args) {
|
|
22
|
-
const command = args[0] || 'help'; let port = 3086, open = true, portExplicit = false;
|
|
23
|
+
const command = args[0] || 'help'; let port = 3086, open = true, portExplicit = false, check = false;
|
|
23
24
|
for (let i = 1; i < args.length; i++) {
|
|
24
25
|
if (args[i] === '--no-open') open = false;
|
|
26
|
+
else if (args[i] === '--check') check = true;
|
|
25
27
|
else if (args[i] === '--port') { const value = args[++i]; if (!/^\d+$/.test(value || '') || +value < 1024 || +value > 65535) throw new Error('--port must be between 1024 and 65535'); port = +value; portExplicit = true; }
|
|
26
28
|
else throw new Error(`Unknown option: ${args[i]}`);
|
|
27
29
|
}
|
|
28
|
-
if (!['setup','start','stop','status','open','help','--help','-h'].includes(command)) throw new Error(`Unknown command: ${command}`);
|
|
29
|
-
|
|
30
|
+
if (!['setup','start','stop','status','open','update','version','help','--help','-h'].includes(command)) throw new Error(`Unknown command: ${command}`);
|
|
31
|
+
if (check && command !== 'update') throw new Error('--check is only valid with daiti update');
|
|
32
|
+
return { command, port, open, portExplicit, check };
|
|
30
33
|
}
|
|
31
34
|
export function runtimeBin() {
|
|
32
35
|
const require = createRequire(path.join(root, 'package.json'));
|
|
@@ -47,6 +50,53 @@ function run(args, p) {
|
|
|
47
50
|
child.once('error', reject); child.once('exit', code => code === 0 ? resolve() : reject(new Error(`Harness setup exited ${code}`)));
|
|
48
51
|
});
|
|
49
52
|
}
|
|
53
|
+
const execFileAsync = promisify(execFile);
|
|
54
|
+
/** Team runs on the experimental Agent Team profile plus its Web panel, in that order. */
|
|
55
|
+
const teamBundles = ['@deepseek-ai/dsh-experimental-agent-team-profile', '@deepseek-ai/dsh-experimental-agent-team-web-profile'];
|
|
56
|
+
/** The Team layer must sit on the IM bundles; the adapter last so its disable wins. */
|
|
57
|
+
const bundleOrder = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@xmanrui/dsh-im', ...teamBundles, '@tanweilabs/daiti-loading', '@tanweilabs/daiti-preferences', '@tanweilabs/daiti-ui'];
|
|
58
|
+
/**
|
|
59
|
+
* Order the profile's bundle layers so each patch applies on the layer it targets.
|
|
60
|
+
*
|
|
61
|
+
* Two orderings are load-bearing. `@xmanrui/dsh-im` must precede
|
|
62
|
+
* `@tanweilabs/daiti-im`, because the adapter disables the upstream IM entry it
|
|
63
|
+
* replaces; reversed, both register the `dshIm` service and the profile fails to
|
|
64
|
+
* load. The Agent Team layer must precede the UI layer, which patches rows the
|
|
65
|
+
* Team layer installs. Unknown bundles keep their relative position at the end.
|
|
66
|
+
*/
|
|
67
|
+
export function orderedBundles(bundles, available = () => true) {
|
|
68
|
+
const unique = [...new Set(bundles)];
|
|
69
|
+
for (const name of ['@xmanrui/dsh-im', '@tanweilabs/daiti-im']) {
|
|
70
|
+
if (unique.includes(name) && !available(name)) throw new Error(`Profile bundle ${name} is unavailable; run npm ci.`);
|
|
71
|
+
}
|
|
72
|
+
// Package manifest exports are not a reliable availability check: built-in
|
|
73
|
+
// bundles resolve through Harness, and some plugins do not export package.json.
|
|
74
|
+
// Preserve configured layers; let the Harness loader resolve or report them.
|
|
75
|
+
const known = bundleOrder.filter(name => unique.includes(name));
|
|
76
|
+
if (known.includes('@xmanrui/dsh-im') && known.includes('@tanweilabs/daiti-im') && known.indexOf('@xmanrui/dsh-im') > known.indexOf('@tanweilabs/daiti-im')) throw new Error('Profile bundle order would register the IM service twice.');
|
|
77
|
+
return [...known, ...unique.filter(name => !bundleOrder.includes(name))];
|
|
78
|
+
}
|
|
79
|
+
async function pinTeamBundles(profile) {
|
|
80
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
81
|
+
const versions = {};
|
|
82
|
+
for (const name of teamBundles) {
|
|
83
|
+
try { versions[name] = JSON.parse(await fs.readFile(require.resolve(`${name}/package.json`), 'utf8')).version; }
|
|
84
|
+
catch { return { pinned: false, reason: `cannot resolve ${name}; install dependencies with npm ci` }; }
|
|
85
|
+
}
|
|
86
|
+
const manifestPath = path.join(profile, 'package.json');
|
|
87
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
88
|
+
const dependencies = { ...manifest.dependencies };
|
|
89
|
+
let changed = false;
|
|
90
|
+
for (const [name, version] of Object.entries(versions)) {
|
|
91
|
+
if (dependencies[name] !== version) { dependencies[name] = version; changed = true; }
|
|
92
|
+
}
|
|
93
|
+
if (!changed) return { pinned: true, changed: false };
|
|
94
|
+
manifest.dependencies = dependencies;
|
|
95
|
+
await atomicJson(manifestPath, manifest);
|
|
96
|
+
// `install` runs after `shrinkwrap` in the official plugin command, so mirror both.
|
|
97
|
+
await execFileAsync('pnpm', ['install', '--ignore-scripts'], { cwd: profile, env: { ...process.env, PATH: runtimePath() } });
|
|
98
|
+
return { pinned: true, changed: true };
|
|
99
|
+
}
|
|
50
100
|
export async function control(state, action = 'status') {
|
|
51
101
|
if (!state) return null;
|
|
52
102
|
try {
|
|
@@ -79,6 +129,10 @@ export async function setup(p) {
|
|
|
79
129
|
const profileData=await readJson(profileManifest);
|
|
80
130
|
const bundles=profileData.dsh.profile.bundles;
|
|
81
131
|
for(const plugin of manifest.plugins)if(!bundles.includes(plugin.name))bundles.push(plugin.name);
|
|
132
|
+
const team=await pinTeamBundles(profile);
|
|
133
|
+
if(team.pinned){for(const name of teamBundles)if(!bundles.includes(name))bundles.push(name);}
|
|
134
|
+
else console.warn(`Agent Team unavailable: ${team.reason}. Starting without Team tools.`);
|
|
135
|
+
profileData.dsh.profile.bundles=orderedBundles(bundles,name=>{try{createRequire(path.join(profile,'package.json')).resolve(`${name}/package.json`);return true;}catch{return false;}});
|
|
82
136
|
await atomicJson(profileManifest,profileData);
|
|
83
137
|
const patch = path.join(profile, 'cordis.patch.yml');
|
|
84
138
|
try { await fs.copyFile(patch, `${patch}.before-daiti-${Date.now()}`); } catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
@@ -96,12 +150,76 @@ export async function openUrl(url) {
|
|
|
96
150
|
const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
|
|
97
151
|
await new Promise((resolve, reject) => { const child = spawn(cmd, args, { stdio: 'ignore' }); child.on('error', reject); child.on('exit', code => code === 0 ? resolve() : reject(new Error('Browser could not be opened.'))); });
|
|
98
152
|
}
|
|
153
|
+
/** Version of the package this process was loaded from. */
|
|
154
|
+
export function runningVersion(directory = root) { try { return createRequire(path.join(root, 'package.json'))(path.join(directory, 'package.json')).version; } catch { return 'unknown'; } }
|
|
155
|
+
/** Where `npm install -g` put this package, so an update can restart the new one. */
|
|
156
|
+
export async function globalRoot() {
|
|
157
|
+
try { const { stdout } = await execFileAsync('npm', ['root', '-g']); return path.join(stdout.trim(), '@tanweilabs', 'daiti'); } catch { return null; }
|
|
158
|
+
}
|
|
159
|
+
/** Version installed for `npm install -g`, however that prefix resolves. */
|
|
160
|
+
export async function installedVersion() {
|
|
161
|
+
const directory = await globalRoot();
|
|
162
|
+
if (directory) { const version = await runningVersion(directory); if (version !== 'unknown') return version; }
|
|
163
|
+
return runningVersion();
|
|
164
|
+
}
|
|
165
|
+
/** Latest published version, or null when the registry cannot be reached. */
|
|
166
|
+
export async function latestVersion() {
|
|
167
|
+
try { const { stdout } = await execFileAsync('npm', ['view', '@tanweilabs/daiti', 'dist-tags.latest']); return stdout.trim() || null; } catch { return null; }
|
|
168
|
+
}
|
|
169
|
+
/** Compare semver-ish strings; a prerelease sorts before its release. */
|
|
170
|
+
export function compareVersions(left, right) {
|
|
171
|
+
const parse = value => String(value).split('-')[0].split('.').map(part => Number.parseInt(part, 10) || 0);
|
|
172
|
+
const a = parse(left); const b = parse(right);
|
|
173
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) { if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0) ? 1 : -1; }
|
|
174
|
+
return left === right ? 0 : left.includes('-') ? -1 : 1;
|
|
175
|
+
}
|
|
176
|
+
/** One-line summary of what version this machine runs, for humans and the log. */
|
|
177
|
+
export async function versionNote(service) {
|
|
178
|
+
const running = service ? (service.version || 'unknown') : runningVersion();
|
|
179
|
+
const latest = await latestVersion();
|
|
180
|
+
const head = service ? `代体运行版本 v${running}(已安装 v${runningVersion()})` : `代体已安装 v${running}`;
|
|
181
|
+
if (!latest) return `${head}(无法连接 npm 查询最新版本)`;
|
|
182
|
+
if (running === 'unknown') return `${head},重启后可识别运行版本`;
|
|
183
|
+
if (compareVersions(running, latest) > 0) return `${head}(本地版本;npm 最新 v${latest})`;
|
|
184
|
+
if (compareVersions(latest, running) > 0) return `${head} → 有新版本 v${latest},运行 daiti update 升级`;
|
|
185
|
+
return `${head}(已是最新)`;
|
|
186
|
+
}
|
|
187
|
+
/** Install the published version and restart the service when it was running. */
|
|
188
|
+
export async function update(p, options = {}) {
|
|
189
|
+
const running = runningVersion();
|
|
190
|
+
const latest = await latestVersion();
|
|
191
|
+
if (!latest) throw new Error('无法从 npm 查询最新版本;检查网络后重试。');
|
|
192
|
+
const outdated = compareVersions(latest, running) > 0;
|
|
193
|
+
if (options.check) { console.log(outdated ? `有新版本:v${running} → v${latest}` : `已是最新:v${running}`); return { running, latest, updated: false }; }
|
|
194
|
+
if (!outdated) { console.log(`已是最新:v${running}`); return { running, latest, updated: false }; }
|
|
195
|
+
const wasRunning = Boolean(await control(await readJson(p.state)));
|
|
196
|
+
console.log(`正在安装 v${latest}…`);
|
|
197
|
+
await execFileAsync('npm', ['install', '-g', '@tanweilabs/daiti@' + latest, '--ignore-scripts']);
|
|
198
|
+
if (!wasRunning) { console.log(`已更新到 v${latest}。下次 daiti start 会用新版本。`); return { running, latest, updated: true }; }
|
|
199
|
+
const state = await readJson(p.state);
|
|
200
|
+
console.log('正在重启服务…');
|
|
201
|
+
await control(state, 'stop');
|
|
202
|
+
for (let i = 0; i < 60; i++) { if (!(await control(state))) break; await pause(250); }
|
|
203
|
+
const global = await globalRoot();
|
|
204
|
+
const entry = global ? path.join(global, 'bin/daiti.mjs') : path.join(root, 'bin/daiti.mjs');
|
|
205
|
+
const child = spawn(process.execPath, [entry, 'start'], { detached: true, stdio: 'ignore' });
|
|
206
|
+
await new Promise(resolve => child.once('spawn', resolve));
|
|
207
|
+
child.unref();
|
|
208
|
+
for (let i = 0; i < 240; i++) {
|
|
209
|
+
const next = await control(await readJson(p.state));
|
|
210
|
+
if (next?.ready) { console.log(`已更新到 v${latest}:http://127.0.0.1:${next.port}/`); return { running, latest, updated: true }; }
|
|
211
|
+
await pause(250);
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`已安装 v${latest},但服务没有就绪;查看 ${p.log}`);
|
|
214
|
+
}
|
|
99
215
|
export async function main(argv = process.argv.slice(2)) {
|
|
100
216
|
const options = parseArgs(argv); const p = paths();
|
|
101
|
-
if (['help','--help','-h'].includes(options.command)) { console.log('daiti
|
|
217
|
+
if (['help','--help','-h'].includes(options.command)) { console.log('daiti start [--port 3086] [--no-open] | stop | status | open | update [--check] | version\nDAITI_HOME selects isolated application data (default ~/.daiti).'); return; }
|
|
102
218
|
if (options.command === 'setup') { await setup(p); console.log(`Daiti prepared: ${p.home}`); return; }
|
|
103
219
|
let current = await control(await readJson(p.state));
|
|
104
|
-
if (options.command === '
|
|
220
|
+
if (options.command === 'version') { console.log(await versionNote()); return; }
|
|
221
|
+
if (options.command === 'update') { await update(p, options); return; }
|
|
222
|
+
if (options.command === 'status') { console.log(await versionNote(current)); console.log(current ? `Daiti ${current.ready ? 'running' : 'starting'}: http://127.0.0.1:${current.port}/` : 'Daiti stopped'); return; }
|
|
105
223
|
if (options.command === 'stop') {
|
|
106
224
|
const state = await readJson(p.state);
|
|
107
225
|
if (!current) { console.log('Daiti stopped (no verified owned service).'); return; }
|
|
@@ -110,11 +228,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
110
228
|
throw new Error('Service is still stopping; no unrelated process was signalled.');
|
|
111
229
|
}
|
|
112
230
|
if (options.command === 'open' && !current?.ready) throw new Error('Daiti is not ready. Run daiti start first.');
|
|
231
|
+
const note = await versionNote();
|
|
113
232
|
if (options.command === 'start' && !current) {
|
|
114
233
|
// Fail clearly before installing anything when a fixed port is occupied.
|
|
115
234
|
if (options.portExplicit) await selectPort(options.port, false);
|
|
116
235
|
await setup(p);
|
|
117
236
|
const log = await fs.open(p.log, 'a', 0o600); await log.chmod(0o600);
|
|
237
|
+
await log.write(`\n${new Date().toISOString()} ${note}\n`);
|
|
118
238
|
const child = spawn(process.execPath, [path.join(root, 'lib/supervisor.mjs'), String(options.port), options.portExplicit ? 'fixed' : 'auto'], { detached: true, stdio: ['ignore', log.fd, log.fd], env: { ...process.env, DAITI_HOME: p.home } });
|
|
119
239
|
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
120
240
|
child.unref(); await log.close();
|
|
@@ -123,5 +243,6 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
123
243
|
}
|
|
124
244
|
if (!current?.ready) throw new Error('Daiti is still starting.');
|
|
125
245
|
if (options.open || options.command === 'open') await openUrl(current.url);
|
|
246
|
+
console.log(note);
|
|
126
247
|
console.log(`Daiti running: http://127.0.0.1:${current.port}/ (use daiti open for authenticated access)`);
|
|
127
248
|
}
|
package/lib/supervisor.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
7
7
|
import { selectPort } from './ports.mjs';
|
|
8
|
-
import { paths, atomicJson, runtimeBin, runtimePath, root } from './cli.mjs';
|
|
8
|
+
import { paths, atomicJson, runtimeBin, runtimePath, root, runningVersion } from './cli.mjs';
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
// Browser launch authentication exchanges a URL token for a cookie.
|
|
@@ -38,6 +38,7 @@ export async function verifyLaunchUrl(value) {
|
|
|
38
38
|
// Only this parent signals its own ChildProcess. CLI clients never signal a PID
|
|
39
39
|
// obtained from disk, so stale state / PID reuse cannot terminate another app.
|
|
40
40
|
export async function supervise({ p = paths(), port, autoPort = false, command = process.execPath, args, timeoutMs = 60000 }) {
|
|
41
|
+
const version = runningVersion();
|
|
41
42
|
const lockPath = path.join(p.home, 'service.lock');
|
|
42
43
|
let lock;
|
|
43
44
|
try { lock = await fs.open(lockPath, 'wx', 0o600); }
|
|
@@ -50,7 +51,7 @@ export async function supervise({ p = paths(), port, autoPort = false, command =
|
|
|
50
51
|
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) { res.writeHead(403).end(); return; }
|
|
51
52
|
res.setHeader('Content-Type', 'application/json');
|
|
52
53
|
if (req.method !== 'POST' || !['/status', '/stop'].includes(req.url)) { res.writeHead(404).end('{}'); return; }
|
|
53
|
-
res.end(JSON.stringify({ instance, ready, port, ...(ready ? { url } : {}) }));
|
|
54
|
+
res.end(JSON.stringify({ instance, ready, port, version, ...(ready ? { url } : {}) }));
|
|
54
55
|
if (req.url === '/stop') stop();
|
|
55
56
|
});
|
|
56
57
|
function stop() {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanweilabs/daiti",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@tanweilabs/daiti",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.5",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@deepseek-ai/dsh": "0.1.6-alpha.2",
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|