@tanweilabs/daiti 0.0.3 → 0.0.10

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 CHANGED
@@ -2,19 +2,29 @@
2
2
 
3
3
  在你的电脑上运行,通过网页或微信使用的个人助理。基于 DeepSeek Harness 和 DSH-IM,通过独立插件提供单主会话、助理设置和首次使用流程。
4
4
 
5
- **0.0.3 为开发者预览版。** 本包不包含个人记忆、历史会话、密钥或 IM 登录数据。
5
+ **0.0.10 为开发者预览版。** 本包不包含个人记忆、历史会话、密钥或 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.3
12
+ npm install -g @tanweilabs/daiti@0.0.10
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,7 @@ PowerShell 对应设置 `$env:DAITI_HOME = "$HOME\.daiti-preview"`。测试时
63
73
 
64
74
  适配包固定 DSH-IM 4.21.0,构建时复用其源码和运行时构造扩展点;不修改原包文件,也不修改 Harness 核心。这些扩展点不是稳定公开 API,上游升级需要契约测试。原许可证保存在适配包中。
65
75
 
66
- 0.0.2 已验证主会话绑定与连接健康状态,微信与网页共用会话已由用户实际验收。0.0.3 在此基础上修复了新装实例的插件层顺序问题(此前全新 `DAITI_HOME` 首次启动会因 `dshIm` 重复注册而失败),并默认启用实验性的 Agent Team 层。
76
+ 0.0.2 已验证主会话绑定与连接健康状态,微信与网页共用会话已由用户实际验收。0.0.3 修复了新装实例的插件层顺序问题(此前全新 `DAITI_HOME` 首次启动会因 `dshIm` 重复注册而失败),并默认启用实验性的 Agent Team 层。0.0.4 增加版本可见与 `daiti update`;0.0.6 重做设置页「助理」的视觉(统一控件、吸顶标题、版本移到页脚、微信区收敛成中性色)。
67
77
 
68
78
  ## Agent Team
69
79
 
@@ -79,7 +89,7 @@ npm run pack:local
79
89
 
80
90
  构建将四个插件打包到 `artifacts/`,生成文件名和 SHA-256 清单。`pack:local` 在 `.local/releases/` 生成 CLI 安装包;包含运行入口、插件包和文档,不包含仓库工作目录、上游源码、个人配置或测试数据。
81
91
 
82
- 未发布的本地安装包可用 `npm install -g /path/to/tanweilabs-daiti-0.0.3.tgz` 安装,随后使用 `daiti start`。安装仍需联网获取声明的运行依赖。
92
+ 未发布的本地安装包可用 `npm install -g /path/to/tanweilabs-daiti-0.0.10.tgz` 安装,随后使用 `daiti start`。安装仍需联网获取声明的运行依赖。
83
93
 
84
94
  - `bin/`、`lib/`:CLI 和后台进程管理。
85
95
  - `packages/assistant-ui/`:界面、首次设置和助理配置。
@@ -98,3 +108,9 @@ npm run pack:local
98
108
  ## 许可证
99
109
 
100
110
  代体代码采用 MIT;依赖保留各自许可证。见 [LICENSE](LICENSE) 和 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。本项目不是 DeepSeek 官方客户端。
111
+
112
+ 发布维护步骤见 [发布指南](docs/releasing.md)。
113
+
114
+ ## 0.0.10 界面更新
115
+
116
+ 协作入口使用右上角多人图标,位于侧栏开关左侧。任务标题与箭头都在右侧栏打开子代理;多个子代理使用同栏标签,重复打开会切回已有标签。主会话保持不变。列表简化为任务名与运行状态;Team 成员尚未合并到此入口。
@@ -2,27 +2,27 @@
2
2
  "plugins": [
3
3
  {
4
4
  "name": "@tanweilabs/daiti-ui",
5
- "version": "0.0.3",
6
- "file": "tanweilabs-daiti-ui-0.0.3-5625684c363a.tgz",
7
- "sha256": "5625684c363a28b912cb6862c2063f2c304dfa02039127a16dd1a7a1b0a7f6e5"
5
+ "version": "0.0.10",
6
+ "file": "tanweilabs-daiti-ui-0.0.10-55c3bdc7b8f4.tgz",
7
+ "sha256": "55c3bdc7b8f41d154ca6a4670ac39652b614c23ef541e15764fd2d1d8575ee67"
8
8
  },
9
9
  {
10
10
  "name": "@tanweilabs/daiti-preferences",
11
- "version": "0.0.3",
12
- "file": "tanweilabs-daiti-preferences-0.0.3-8137cd254b7b.tgz",
13
- "sha256": "8137cd254b7be52ff8219be31bcfd466d50a727b5a888d1b1324e5017a5b0797"
11
+ "version": "0.0.10",
12
+ "file": "tanweilabs-daiti-preferences-0.0.10-039a7bfc652e.tgz",
13
+ "sha256": "039a7bfc652e7fe2b0654a21e522da75eaa9ae15521b38f3651bcfa79aa8b414"
14
14
  },
15
15
  {
16
16
  "name": "@tanweilabs/daiti-loading",
17
- "version": "0.0.3",
18
- "file": "tanweilabs-daiti-loading-0.0.3-b61364395143.tgz",
19
- "sha256": "b613643951436c2e565b953a6a04a7d0c2cdcc619fae6bc98d7cddb535b27b77"
17
+ "version": "0.0.10",
18
+ "file": "tanweilabs-daiti-loading-0.0.10-d84c36d090e1.tgz",
19
+ "sha256": "d84c36d090e155f3c0bc24c3b4b2578964a66749af281ee6c6806bbe19201a00"
20
20
  },
21
21
  {
22
22
  "name": "@tanweilabs/daiti-im",
23
- "version": "0.0.3",
24
- "file": "tanweilabs-daiti-im-0.0.3-52e45084fd59.tgz",
25
- "sha256": "52e45084fd59d83bc9abba3c4a06fcab2578fe7430e99619a1fa490cb339dbb2"
23
+ "version": "0.0.10",
24
+ "file": "tanweilabs-daiti-im-0.0.10-b9437d32fafa.tgz",
25
+ "sha256": "b9437d32fafae6a7f79a603335ebe631d40197b218de8c6f421e9775cb5d9d26"
26
26
  }
27
27
  ]
28
28
  }
@@ -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` 打开认证入口。
@@ -65,3 +65,43 @@
65
65
  - 未验证(发布前已知):真实的浏览器会话里 Team 工具是否按预期出现;"标准模式"预设内自带的子代理工具行不受 profile patch 影响,可能与 Team 的 `list_agents`/`send_message` 同名共存(参数不同),需要真实会话验收;Team 策略会引导 Lead 使用 `wait_agent`(10s–1h),是否加重主会话阻塞待观察。
66
66
 
67
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` 路径。
76
+
77
+ ## 2026-09-22 设置页「助理」视觉重做(0.0.6)
78
+
79
+ - 起因:本人反馈设置页「助理」观感粗糙(标题被裁、控件不统一、微信区与上半页不是一套视觉、版本行位置尴尬、出现「?」方块)。
80
+ - 改动只落在 `packages/assistant-ui/client.jsx`(及其产物 `client.cjs`):导航宽度 188→168px;内容列去掉多余双层内边距;分区标题吸顶;标签/输入框/按钮统一为 32px 高、16px 胶囊圆角的一套控件,保存键用 Harness 主按钮;版本行移到页脚并加发丝线;微信区把上游按 `.dim-panel` 作用域、在设置页里失效的规则按需重述(顺带修掉「?」方块与串行 tooltip),品牌绿收敛为中性色,空态 110px 辉光块压成 64px 中性块。
81
+ - 颜色/圆角/字号全部走 Harness 设计变量并带兜底值;与外壳的耦合点只有一处(内容列 24px 内边距,见代码注释),外壳改动需同步预览文件。
82
+ - 业务逻辑未动:RPC 方法名、字段、保存/初始化、`open-folder`、onboarding 判定、`singleSessionIm` 开关均未修改。
83
+ - `npm test` 41/41 通过;`npm run build` 通过;独立 `DAITI_HOME`(端口 3177)无头 Chrome 实测:页面加载正常、设置页可打开、吸顶生效、点保存返回「已保存。」、控制台零报错;测试实例已停止,用户实例 3086 未接触。
84
+ - 未验证:真实实例中「已接入账号」卡片(验收实例未绑微信,只走空态)、深色主题、模型/通用两页、窄屏、「打开文件夹」(会弹 Finder)。
85
+
86
+ ## 2026-09-22 恢复头部子代理入口(0.0.7)
87
+
88
+ - 起因:本人在原版 Harness 里看到会话标题旁的「/ N 个子代理」入口,问代体为什么没有。查明是代体自己的 CSS 规则 `[data-conversation-header-leading] ~ div:not([data-conversation-header-corner])` 过度隐藏——它把官方 `titleCluster` 一起藏了,而 `conversation.session.header.lineage`(子代理目录)和 Agent Team 按钮都装在那里。
89
+ - 改动:删除该隐藏规则,只保留 `[data-conversation-header-corner]{margin-left:auto}`。`ui-subagent` 一直是挂载的,因此恢复后即可用官方目录:点开看子会话列表,行尾箭头在右侧栏打开(不替换主视图)。
90
+ - 同一版修掉 0.0.6 引入的一个内边距缺陷:`[role="dialog"] *:has(> .daiti-settings-page)` 与 `.daiti-settings-page` 自身同特异性,覆盖掉了设置页的底部内边距;现显式补回 `padding-bottom:24px`。
91
+ - `npm test` 41/41 通过;`npm run build` 通过;四个插件包 0.0.7。
92
+ - 未验证:恢复后头部其余 utility 图标的实际排布(该容器无 data 属性,无法只隐藏它),需在真实浏览器里看过再决定是否收掉。
93
+
94
+ ## 2026-09-22 头部精简:只留品牌与子代理入口(0.0.8)
95
+
96
+ - 起因:0.0.7 恢复子代理入口后,头部同时出现了会话名、Agent Team 面板、在文件中打开、更多操作等入口,本人要求只保留子代理。
97
+ - 方法:先用 CDP 连真实页面 dump 头部 DOM(没有猜选择器),拿到实际 `aria-label` 后写规则,**再注入试算确认命中的正好是目标、且没误伤子代理 chip**,然后才落代码。
98
+ - 结果:隐藏「打开右侧边栏」「更多操作」「选择打开方式」「在 访达 中打开工作目录」四个按钮、面包屑里的会话名(`nav[aria-label] button[disabled]`)、以及没有 `aria-label` 的 Agent Team 按钮(`header button:not([aria-label]):has(> span:not([aria-hidden="true"]))`)。子代理 chip 是头部唯一带 `aria-label` 的文字按钮,因此规则天然把它排除在外。
99
+ - 观察项:Agent Team 只为现阶段收起,未来要用 Team 时删掉那一条即可;`[data-conversation-header-actions]` 属性在上游不存在,已弃用该写法。
100
+ - `npm test` 41/41 通过;`npm run build` 通过;四个插件包 0.0.8。试算证据见本次会话的 CDP 输出。
101
+
102
+ ## 2026-09-22 头部布局修正:恢复右栏按钮、子代理移到品牌右侧(0.0.9)
103
+
104
+ - 起因:0.0.8 收过头了——「打开右侧边栏」也被隐藏(右栏、工作区文件、预览都靠它),且子代理 chip 落到左上角与设置图标挤在一起。
105
+ - 修正:① 恢复「打开右侧边栏」,只隐藏「更多操作」「选择打开方式」「在访达中打开工作目录」;② 子代理 chip 用 `position:fixed` 移到「代体」品牌右侧(`left:calc(50% + var(--daiti-brand-w))`,品牌文案变长时调该变量),不再依赖面包屑容器的布局。
106
+ - 复核方法:CDP 连真实页面 dump 头部按钮的 display 状态,确认右侧边栏按钮可见、chip 位置正确、被隐藏的仍是那三个。
107
+ - `npm test` 41/41 通过;`npm run build` 通过;四个插件包 0.0.9。
package/lib/cli.mjs CHANGED
@@ -20,14 +20,16 @@ export async function atomicJson(file, value) {
20
20
  await fs.rename(temp, file);
21
21
  }
22
22
  export function parseArgs(args) {
23
- 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;
24
24
  for (let i = 1; i < args.length; i++) {
25
25
  if (args[i] === '--no-open') open = false;
26
+ else if (args[i] === '--check') check = true;
26
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; }
27
28
  else throw new Error(`Unknown option: ${args[i]}`);
28
29
  }
29
- if (!['setup','start','stop','status','open','help','--help','-h'].includes(command)) throw new Error(`Unknown command: ${command}`);
30
- return { command, port, open, portExplicit };
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 };
31
33
  }
32
34
  export function runtimeBin() {
33
35
  const require = createRequire(path.join(root, 'package.json'));
@@ -67,7 +69,10 @@ export function orderedBundles(bundles, available = () => true) {
67
69
  for (const name of ['@xmanrui/dsh-im', '@tanweilabs/daiti-im']) {
68
70
  if (unique.includes(name) && !available(name)) throw new Error(`Profile bundle ${name} is unavailable; run npm ci.`);
69
71
  }
70
- const known = bundleOrder.filter(name => unique.includes(name) && available(name));
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));
71
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.');
72
77
  return [...known, ...unique.filter(name => !bundleOrder.includes(name))];
73
78
  }
@@ -145,12 +150,76 @@ export async function openUrl(url) {
145
150
  const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
146
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.'))); });
147
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
+ }
148
215
  export async function main(argv = process.argv.slice(2)) {
149
216
  const options = parseArgs(argv); const p = paths();
150
- if (['help','--help','-h'].includes(options.command)) { console.log('daiti setup | start [--port 3086] [--no-open] | stop | status | open\nDAITI_HOME selects isolated application data (default ~/.daiti).'); return; }
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; }
151
218
  if (options.command === 'setup') { await setup(p); console.log(`Daiti prepared: ${p.home}`); return; }
152
219
  let current = await control(await readJson(p.state));
153
- if (options.command === 'status') { console.log(current ? `Daiti ${current.ready ? 'running' : 'starting'}: http://127.0.0.1:${current.port}/` : 'Daiti stopped'); return; }
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; }
154
223
  if (options.command === 'stop') {
155
224
  const state = await readJson(p.state);
156
225
  if (!current) { console.log('Daiti stopped (no verified owned service).'); return; }
@@ -159,11 +228,13 @@ export async function main(argv = process.argv.slice(2)) {
159
228
  throw new Error('Service is still stopping; no unrelated process was signalled.');
160
229
  }
161
230
  if (options.command === 'open' && !current?.ready) throw new Error('Daiti is not ready. Run daiti start first.');
231
+ const note = await versionNote();
162
232
  if (options.command === 'start' && !current) {
163
233
  // Fail clearly before installing anything when a fixed port is occupied.
164
234
  if (options.portExplicit) await selectPort(options.port, false);
165
235
  await setup(p);
166
236
  const log = await fs.open(p.log, 'a', 0o600); await log.chmod(0o600);
237
+ await log.write(`\n${new Date().toISOString()} ${note}\n`);
167
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 } });
168
239
  await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
169
240
  child.unref(); await log.close();
@@ -172,5 +243,6 @@ export async function main(argv = process.argv.slice(2)) {
172
243
  }
173
244
  if (!current?.ready) throw new Error('Daiti is still starting.');
174
245
  if (options.open || options.command === 'open') await openUrl(current.url);
246
+ console.log(note);
175
247
  console.log(`Daiti running: http://127.0.0.1:${current.port}/ (use daiti open for authenticated access)`);
176
248
  }
@@ -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() {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@tanweilabs/daiti",
3
- "version": "0.0.3",
3
+ "version": "0.0.10",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@tanweilabs/daiti",
9
- "version": "0.0.3",
9
+ "version": "0.0.10",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@deepseek-ai/dsh": "0.1.6-alpha.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanweilabs/daiti",
3
- "version": "0.0.3",
3
+ "version": "0.0.10",
4
4
  "description": "代体:本地运行、通过网页和 IM 使用的个人助理。",
5
5
  "type": "module",
6
6
  "license": "MIT",