@tanweilabs/daiti 0.0.1 → 0.0.3
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 +19 -11
- package/THIRD_PARTY_NOTICES.md +3 -1
- package/artifacts/manifest.json +15 -9
- package/artifacts/tanweilabs-daiti-im-0.0.3-52e45084fd59.tgz +0 -0
- package/artifacts/tanweilabs-daiti-loading-0.0.3-b61364395143.tgz +0 -0
- package/artifacts/{tanweilabs-daiti-preferences-0.0.1.tgz → tanweilabs-daiti-preferences-0.0.3-8137cd254b7b.tgz} +0 -0
- package/artifacts/tanweilabs-daiti-ui-0.0.3-5625684c363a.tgz +0 -0
- package/docs/validation.md +43 -0
- package/lib/cli.mjs +65 -7
- package/lib/ports.mjs +23 -0
- package/lib/supervisor.mjs +6 -2
- package/npm-shrinkwrap.json +1691 -8
- package/package.json +9 -3
- package/artifacts/tanweilabs-daiti-loading-0.0.1.tgz +0 -0
- package/artifacts/tanweilabs-daiti-ui-0.0.1.tgz +0 -0
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# 代体
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
在你的电脑上运行,通过网页或微信使用的个人助理。基于 DeepSeek Harness 和 DSH-IM,通过独立插件提供单主会话、助理设置和首次使用流程。
|
|
4
4
|
|
|
5
|
-
**0.0.
|
|
5
|
+
**0.0.3 为开发者预览版。** 本包不包含个人记忆、历史会话、密钥或 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.3
|
|
13
13
|
daiti start
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -36,7 +36,7 @@ node bin/daiti.mjs stop
|
|
|
36
36
|
node bin/daiti.mjs start --port 3086 --no-open
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
默认端口 3086
|
|
39
|
+
默认端口 3086,只监听本机;默认端口被占用时自动选择空闲端口,并打开实际地址。同一 DAITI_HOME 已有服务时直接复用;显式指定 `--port` 时不会自动换端口,占用则提示错误。可通过 `daiti status` 查看实际端口。关闭浏览器后后台服务继续运行;停止服务或电脑休眠、关机时不能保证 IM 响应。停止前请等待正在执行的任务完成。当前没有开机自启、系统服务或自动升级。
|
|
40
40
|
|
|
41
41
|
## 数据在哪里
|
|
42
42
|
|
|
@@ -55,11 +55,19 @@ DAITI_HOME="$HOME/.daiti-preview" node bin/daiti.mjs stop
|
|
|
55
55
|
|
|
56
56
|
PowerShell 对应设置 `$env:DAITI_HOME = "$HOME\.daiti-preview"`。测试时选择新的工作文件夹;不要在两个运行实例中连接同一个 IM 机器人。
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## 微信连接
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
首版仅支持微信,在“设置 → 助理 → 微信助理”中扫码。连接启动前,代体通过上游自身的绑定方法把扫码账号本人的私聊固定到网页主会话。缺少主会话或目录校验失败时连接报错,不创建替代会话;工作目录和模型由助理统一管理。群聊和非本人消息不接入个人主会话。
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
既有错误绑定可在重连时纠正,旧会话历史保留。绑定跨工作区时,上游可能重置该机器人其他会话关联,迁移前需备份。`/new`、`/session` 等切换或配置命令在单助理模式下不可用。
|
|
63
|
+
|
|
64
|
+
适配包固定 DSH-IM 4.21.0,构建时复用其源码和运行时构造扩展点;不修改原包文件,也不修改 Harness 核心。这些扩展点不是稳定公开 API,上游升级需要契约测试。原许可证保存在适配包中。
|
|
65
|
+
|
|
66
|
+
0.0.2 已验证主会话绑定与连接健康状态,微信与网页共用会话已由用户实际验收。0.0.3 在此基础上修复了新装实例的插件层顺序问题(此前全新 `DAITI_HOME` 首次启动会因 `dshIm` 重复注册而失败),并默认启用实验性的 Agent Team 层。
|
|
67
|
+
|
|
68
|
+
## Agent Team
|
|
69
|
+
|
|
70
|
+
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` 不受影响),而"标准模式"预设内的子代理工具是否同时生效仍待真实会话验收。
|
|
63
71
|
|
|
64
72
|
## 开发和本地打包
|
|
65
73
|
|
|
@@ -69,9 +77,9 @@ npm run build
|
|
|
69
77
|
npm run pack:local
|
|
70
78
|
```
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
构建将四个插件打包到 `artifacts/`,生成文件名和 SHA-256 清单。`pack:local` 在 `.local/releases/` 生成 CLI 安装包;包含运行入口、插件包和文档,不包含仓库工作目录、上游源码、个人配置或测试数据。
|
|
73
81
|
|
|
74
|
-
未发布的本地安装包可用 `npm install -g /path/to/tanweilabs-daiti-0.0.
|
|
82
|
+
未发布的本地安装包可用 `npm install -g /path/to/tanweilabs-daiti-0.0.3.tgz` 安装,随后使用 `daiti start`。安装仍需联网获取声明的运行依赖。
|
|
75
83
|
|
|
76
84
|
- `bin/`、`lib/`:CLI 和后台进程管理。
|
|
77
85
|
- `packages/assistant-ui/`:界面、首次设置和助理配置。
|
|
@@ -83,9 +91,9 @@ npm run pack:local
|
|
|
83
91
|
|
|
84
92
|
## 版本和限制
|
|
85
93
|
|
|
86
|
-
|
|
94
|
+
当前本地代码固定 Harness `0.1.6-alpha.2`、DSH-IM `4.21.0`。部分界面调整依赖这个版本的插槽、DOM 结构和中文标签;上游升级必须先验收,不能承诺跨版本无改动兼容。
|
|
87
95
|
|
|
88
|
-
完整验证记录见 [docs/validation.md](docs/validation.md)。Windows/Linux
|
|
96
|
+
完整验证记录见 [docs/validation.md](docs/validation.md)。Windows/Linux 尚需真实机器安装验证;微信跨端会话已在本机验收,其他平台不在本版支持范围内。升级机制、异常断电恢复和工作目录迁移仍待完善。
|
|
89
97
|
|
|
90
98
|
## 许可证
|
|
91
99
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -6,4 +6,6 @@
|
|
|
6
6
|
- [DSH-IM](https://github.com/xmanrui/dsh-im), 4.21.0, MIT. Installed as a separate, unmodified dependency with its original license.
|
|
7
7
|
- pnpm, 11.19.0, MIT; esbuild, 0.25.12, MIT (build-time).
|
|
8
8
|
|
|
9
|
-
The executable package declares the runtime dependencies; setup installs the
|
|
9
|
+
The executable package declares the runtime dependencies; setup installs the four local plugin archives and pinned DSH-IM into a separate data directory. No personal memory, session history, model credential or IM login data belongs in this repository or its release artifacts.
|
|
10
|
+
|
|
11
|
+
The local single-session WeChat fix bundles selected DSH-IM source through its version-pinned runtime constructor seams. The upstream package files are not edited. Its MIT notice is retained as UPSTREAM-LICENSE in the adapter and assistant UI packages.
|
package/artifacts/manifest.json
CHANGED
|
@@ -2,21 +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.3",
|
|
6
|
+
"file": "tanweilabs-daiti-ui-0.0.3-5625684c363a.tgz",
|
|
7
|
+
"sha256": "5625684c363a28b912cb6862c2063f2c304dfa02039127a16dd1a7a1b0a7f6e5"
|
|
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.3",
|
|
12
|
+
"file": "tanweilabs-daiti-preferences-0.0.3-8137cd254b7b.tgz",
|
|
13
|
+
"sha256": "8137cd254b7be52ff8219be31bcfd466d50a727b5a888d1b1324e5017a5b0797"
|
|
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.3",
|
|
18
|
+
"file": "tanweilabs-daiti-loading-0.0.3-b61364395143.tgz",
|
|
19
|
+
"sha256": "b613643951436c2e565b953a6a04a7d0c2cdcc619fae6bc98d7cddb535b27b77"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "@tanweilabs/daiti-im",
|
|
23
|
+
"version": "0.0.3",
|
|
24
|
+
"file": "tanweilabs-daiti-im-0.0.3-52e45084fd59.tgz",
|
|
25
|
+
"sha256": "52e45084fd59d83bc9abba3c4a06fcab2578fe7430e99619a1fa490cb339dbb2"
|
|
20
26
|
}
|
|
21
27
|
]
|
|
22
28
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/validation.md
CHANGED
|
@@ -22,3 +22,46 @@
|
|
|
22
22
|
- UI适配依赖当前Harness版本的插槽及部分DOM结构,升级前需重复浏览器验收。
|
|
23
23
|
|
|
24
24
|
现有个人代体与历史测试实例不属于本次验收数据,未被迁移、重置或停止。公开发布仓库或安装包仍需单独确认。
|
|
25
|
+
|
|
26
|
+
## 微信单主会话本地修复
|
|
27
|
+
|
|
28
|
+
18项自动测试通过,其中9项为IM路由、本人私聊限制、缺主会话拒绝、目录同一性及真实上游 ask 路径。隔离 Host probe 已通过实际 modernHarnessApi、HarnessClient 和 BotWorkspaceStore 的绑定及持久化回读。真实账号启动时发现大小写不同但指向同目录的路径误判,改为文件系统身份比较并增加回归测试。修复后账号显示1/1在线,状态healthy且error为空,绑定ID与网页主ID一致;旧独立微信会话文件哈希仍与备份一致。未主动发送真实微信消息;跨端模型回复待用户验证。本地修复未发布npm。
|
|
29
|
+
|
|
30
|
+
## 2026-09-21 自动端口选择(随 0.0.2 发布)
|
|
31
|
+
|
|
32
|
+
- 默认启动先尝试 3086;被占用时由操作系统分配空闲本机端口,实际端口用于就绪校验、状态查询和浏览器地址。显式 `--port` 冲突时明确报错。
|
|
33
|
+
- 同一 DAITI_HOME 的已认证服务优先复用,不因端口参数重复启动。
|
|
34
|
+
- `npm test` 20 项通过,`npm run build` 通过。
|
|
35
|
+
- 独立 `.local/acceptance` 实例真实启动:3086 已占用时选择 55957,重复启动保持同一实例;停止后显式 3086 报错且未创建服务;再次默认启动选择 56049,主会话 ID 不变;最后测试服务已停止,个人 3086 服务仍就绪。
|
|
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)
|
package/lib/cli.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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';
|
|
9
|
+
import { selectPort } from './ports.mjs';
|
|
8
10
|
export const root = fileURLToPath(new URL('../', import.meta.url));
|
|
9
11
|
export const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
10
12
|
export function paths(env = process.env) {
|
|
@@ -18,14 +20,14 @@ export async function atomicJson(file, value) {
|
|
|
18
20
|
await fs.rename(temp, file);
|
|
19
21
|
}
|
|
20
22
|
export function parseArgs(args) {
|
|
21
|
-
const command = args[0] || 'help'; let port = 3086, open = true;
|
|
23
|
+
const command = args[0] || 'help'; let port = 3086, open = true, portExplicit = false;
|
|
22
24
|
for (let i = 1; i < args.length; i++) {
|
|
23
25
|
if (args[i] === '--no-open') open = false;
|
|
24
|
-
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; }
|
|
26
|
+
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; }
|
|
25
27
|
else throw new Error(`Unknown option: ${args[i]}`);
|
|
26
28
|
}
|
|
27
29
|
if (!['setup','start','stop','status','open','help','--help','-h'].includes(command)) throw new Error(`Unknown command: ${command}`);
|
|
28
|
-
return { command, port, open };
|
|
30
|
+
return { command, port, open, portExplicit };
|
|
29
31
|
}
|
|
30
32
|
export function runtimeBin() {
|
|
31
33
|
const require = createRequire(path.join(root, 'package.json'));
|
|
@@ -46,6 +48,50 @@ function run(args, p) {
|
|
|
46
48
|
child.once('error', reject); child.once('exit', code => code === 0 ? resolve() : reject(new Error(`Harness setup exited ${code}`)));
|
|
47
49
|
});
|
|
48
50
|
}
|
|
51
|
+
const execFileAsync = promisify(execFile);
|
|
52
|
+
/** Team runs on the experimental Agent Team profile plus its Web panel, in that order. */
|
|
53
|
+
const teamBundles = ['@deepseek-ai/dsh-experimental-agent-team-profile', '@deepseek-ai/dsh-experimental-agent-team-web-profile'];
|
|
54
|
+
/** The Team layer must sit on the IM bundles; the adapter last so its disable wins. */
|
|
55
|
+
const bundleOrder = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@xmanrui/dsh-im', ...teamBundles, '@tanweilabs/daiti-loading', '@tanweilabs/daiti-preferences', '@tanweilabs/daiti-ui'];
|
|
56
|
+
/**
|
|
57
|
+
* Order the profile's bundle layers so each patch applies on the layer it targets.
|
|
58
|
+
*
|
|
59
|
+
* Two orderings are load-bearing. `@xmanrui/dsh-im` must precede
|
|
60
|
+
* `@tanweilabs/daiti-im`, because the adapter disables the upstream IM entry it
|
|
61
|
+
* replaces; reversed, both register the `dshIm` service and the profile fails to
|
|
62
|
+
* load. The Agent Team layer must precede the UI layer, which patches rows the
|
|
63
|
+
* Team layer installs. Unknown bundles keep their relative position at the end.
|
|
64
|
+
*/
|
|
65
|
+
export function orderedBundles(bundles, available = () => true) {
|
|
66
|
+
const unique = [...new Set(bundles)];
|
|
67
|
+
for (const name of ['@xmanrui/dsh-im', '@tanweilabs/daiti-im']) {
|
|
68
|
+
if (unique.includes(name) && !available(name)) throw new Error(`Profile bundle ${name} is unavailable; run npm ci.`);
|
|
69
|
+
}
|
|
70
|
+
const known = bundleOrder.filter(name => unique.includes(name) && available(name));
|
|
71
|
+
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.');
|
|
72
|
+
return [...known, ...unique.filter(name => !bundleOrder.includes(name))];
|
|
73
|
+
}
|
|
74
|
+
async function pinTeamBundles(profile) {
|
|
75
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
76
|
+
const versions = {};
|
|
77
|
+
for (const name of teamBundles) {
|
|
78
|
+
try { versions[name] = JSON.parse(await fs.readFile(require.resolve(`${name}/package.json`), 'utf8')).version; }
|
|
79
|
+
catch { return { pinned: false, reason: `cannot resolve ${name}; install dependencies with npm ci` }; }
|
|
80
|
+
}
|
|
81
|
+
const manifestPath = path.join(profile, 'package.json');
|
|
82
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
83
|
+
const dependencies = { ...manifest.dependencies };
|
|
84
|
+
let changed = false;
|
|
85
|
+
for (const [name, version] of Object.entries(versions)) {
|
|
86
|
+
if (dependencies[name] !== version) { dependencies[name] = version; changed = true; }
|
|
87
|
+
}
|
|
88
|
+
if (!changed) return { pinned: true, changed: false };
|
|
89
|
+
manifest.dependencies = dependencies;
|
|
90
|
+
await atomicJson(manifestPath, manifest);
|
|
91
|
+
// `install` runs after `shrinkwrap` in the official plugin command, so mirror both.
|
|
92
|
+
await execFileAsync('pnpm', ['install', '--ignore-scripts'], { cwd: profile, env: { ...process.env, PATH: runtimePath() } });
|
|
93
|
+
return { pinned: true, changed: true };
|
|
94
|
+
}
|
|
49
95
|
export async function control(state, action = 'status') {
|
|
50
96
|
if (!state) return null;
|
|
51
97
|
try {
|
|
@@ -72,15 +118,25 @@ export async function setup(p) {
|
|
|
72
118
|
await fs.copyFile(path.resolve(root, 'artifacts', plugin.file), destination);
|
|
73
119
|
return destination;
|
|
74
120
|
}));
|
|
75
|
-
await run(['plugin', '--profile', 'web', 'add', '@xmanrui/dsh-im@4.21.0', ...packages], p);
|
|
121
|
+
await run(['plugin', '--profile', 'web', 'add', '--ignore-scripts', '@xmanrui/dsh-im@4.21.0', ...packages], p);
|
|
76
122
|
const profile = path.join(p.runtime, 'profiles/web');
|
|
123
|
+
const profileManifest=path.join(profile,'package.json');
|
|
124
|
+
const profileData=await readJson(profileManifest);
|
|
125
|
+
const bundles=profileData.dsh.profile.bundles;
|
|
126
|
+
for(const plugin of manifest.plugins)if(!bundles.includes(plugin.name))bundles.push(plugin.name);
|
|
127
|
+
const team=await pinTeamBundles(profile);
|
|
128
|
+
if(team.pinned){for(const name of teamBundles)if(!bundles.includes(name))bundles.push(name);}
|
|
129
|
+
else console.warn(`Agent Team unavailable: ${team.reason}. Starting without Team tools.`);
|
|
130
|
+
profileData.dsh.profile.bundles=orderedBundles(bundles,name=>{try{createRequire(path.join(profile,'package.json')).resolve(`${name}/package.json`);return true;}catch{return false;}});
|
|
131
|
+
await atomicJson(profileManifest,profileData);
|
|
77
132
|
const patch = path.join(profile, 'cordis.patch.yml');
|
|
78
133
|
try { await fs.copyFile(patch, `${patch}.before-daiti-${Date.now()}`); } catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
79
134
|
// Our overlay is separate from the profile's user-owned patch.
|
|
80
135
|
await atomicJson(path.join(p.home, 'daiti.patch.json'), [
|
|
81
136
|
{ id: 'ui-sidebar', disabled: true },
|
|
137
|
+
{ id: 'daiti-single-session-im', disabled: false, config: { settingsPath: path.join(p.home, 'settings.json') } },
|
|
82
138
|
{ id: 'daiti-onboarding-preferences', disabled: false, config: { settingsPath: path.join(p.home, 'settings.json') } },
|
|
83
|
-
{ id: 'daiti-ui', disabled: false, config: { settingsPath: path.join(p.home, 'settings.json'), defaultWorkspacePath: path.join(homedir(), 'Daiti') } },
|
|
139
|
+
{ id: 'daiti-ui', disabled: false, config: { settingsPath: path.join(p.home, 'settings.json'), defaultWorkspacePath: path.join(homedir(), 'Daiti'), singleSessionIm: true } },
|
|
84
140
|
]);
|
|
85
141
|
await atomicJson(marker, { signature, installedAt: new Date().toISOString() });
|
|
86
142
|
}
|
|
@@ -104,9 +160,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
104
160
|
}
|
|
105
161
|
if (options.command === 'open' && !current?.ready) throw new Error('Daiti is not ready. Run daiti start first.');
|
|
106
162
|
if (options.command === 'start' && !current) {
|
|
163
|
+
// Fail clearly before installing anything when a fixed port is occupied.
|
|
164
|
+
if (options.portExplicit) await selectPort(options.port, false);
|
|
107
165
|
await setup(p);
|
|
108
166
|
const log = await fs.open(p.log, 'a', 0o600); await log.chmod(0o600);
|
|
109
|
-
const child = spawn(process.execPath, [path.join(root, 'lib/supervisor.mjs'), String(options.port)], { detached: true, stdio: ['ignore', log.fd, log.fd], env: { ...process.env, DAITI_HOME: p.home } });
|
|
167
|
+
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 } });
|
|
110
168
|
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
111
169
|
child.unref(); await log.close();
|
|
112
170
|
for (let i = 0; i < 240; i++) { current = await control(await readJson(p.state)); if (current?.ready) break; if (child.exitCode !== null) throw new Error(`Daiti exited during startup; inspect ${p.log}`); await pause(250); }
|
package/lib/ports.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
|
|
3
|
+
// Probe the same loopback address used by Harness. Port 0 lets the OS choose
|
|
4
|
+
// an available port; no assumptions about neighbouring ports being free.
|
|
5
|
+
async function probe(port) {
|
|
6
|
+
const server = createServer();
|
|
7
|
+
await new Promise((resolve, reject) => {
|
|
8
|
+
server.once('error', reject);
|
|
9
|
+
server.listen({ port, host: '127.0.0.1', exclusive: true }, resolve);
|
|
10
|
+
});
|
|
11
|
+
const selected = server.address().port;
|
|
12
|
+
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
|
13
|
+
return selected;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function selectPort(port, automatic = false) {
|
|
17
|
+
try { return await probe(port); }
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code !== 'EADDRINUSE') throw error;
|
|
20
|
+
if (automatic) return probe(0);
|
|
21
|
+
throw new Error(`Port ${port} is already in use. Choose another --port, or omit --port to select automatically.`, { cause: error });
|
|
22
|
+
}
|
|
23
|
+
}
|
package/lib/supervisor.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { promises as fs } from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
7
|
+
import { selectPort } from './ports.mjs';
|
|
7
8
|
import { paths, atomicJson, runtimeBin, runtimePath, root } from './cli.mjs';
|
|
8
9
|
|
|
9
10
|
|
|
@@ -36,7 +37,7 @@ export async function verifyLaunchUrl(value) {
|
|
|
36
37
|
|
|
37
38
|
// Only this parent signals its own ChildProcess. CLI clients never signal a PID
|
|
38
39
|
// obtained from disk, so stale state / PID reuse cannot terminate another app.
|
|
39
|
-
export async function supervise({ p = paths(), port, command = process.execPath, args, timeoutMs = 60000 }) {
|
|
40
|
+
export async function supervise({ p = paths(), port, autoPort = false, command = process.execPath, args, timeoutMs = 60000 }) {
|
|
40
41
|
const lockPath = path.join(p.home, 'service.lock');
|
|
41
42
|
let lock;
|
|
42
43
|
try { lock = await fs.open(lockPath, 'wx', 0o600); }
|
|
@@ -60,6 +61,9 @@ export async function supervise({ p = paths(), port, command = process.execPath,
|
|
|
60
61
|
} else server.close();
|
|
61
62
|
}
|
|
62
63
|
try {
|
|
64
|
+
const requestedPort = port;
|
|
65
|
+
port = await selectPort(port, autoPort);
|
|
66
|
+
if (port !== requestedPort) console.log(`Port ${requestedPort} is occupied; using ${port}.`);
|
|
63
67
|
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
|
|
64
68
|
await atomicJson(p.state, { instance, secret, controlPort: server.address().port, pid: process.pid });
|
|
65
69
|
child = spawn(command, args || [runtimeBin(), '--profile', 'web', '--patch', path.join(p.home, 'daiti.patch.json'), '--host', '127.0.0.1', '--port', String(port), '--no-open'], { env: { ...process.env, DSH_HOME: p.runtime, PATH: runtimePath() }, cwd: p.home, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -87,5 +91,5 @@ export async function supervise({ p = paths(), port, command = process.execPath,
|
|
|
87
91
|
} finally { server.close(); await cleanup(); }
|
|
88
92
|
}
|
|
89
93
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
90
|
-
supervise({ port: Number(process.argv[2]) }).catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
94
|
+
supervise({ port: Number(process.argv[2]), autoPort: process.argv[3] === 'auto' }).catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
91
95
|
}
|