Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
411d09b3a8 | ||
|
|
af0385451a | ||
|
|
a61bd70399 | ||
|
|
b329de3bf5 | ||
|
|
b067b57975 | ||
|
|
c6464bc4b0 | ||
|
|
65f7753a79 | ||
|
|
8712fadb49 | ||
|
|
64837575a9 | ||
|
|
765789f34d | ||
|
|
754ce83ffd | ||
|
|
22a87232d7 | ||
|
|
fb219e04a3 | ||
|
|
642299ddd4 |
+2
-1
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
# 无斜杠形式同时忽略目录与符号链接(目录式规则不匹配 symlink,曾致链接被提交)。
|
||||
node_modules
|
||||
dist/
|
||||
*.local
|
||||
|
||||
@@ -17,3 +17,20 @@
|
||||
- 模块边界、公开契约与隔离档位见 `policy/modules/`(如有)与 PROJECT.md。
|
||||
- 跨仓 gitlink 变更须经组合验证后在 Dev 仓推进;受保护分支经 PR 合入。
|
||||
- 验证命令见 PROJECT.md「验证」节;完成声明须附实际命令输出证据。
|
||||
|
||||
## Gitea 与 Git 访问事实(2026-09-30 实测)
|
||||
|
||||
历史文档与旧链接里的 `git-panel.u1680832.nyat.app:30319` 已失效(TLS 握手失败),
|
||||
见到一律按过期处理,不要再试。当前权威通道:
|
||||
|
||||
- **Web / API(HTTPS)**:`https://git-home.u1680832.nyat.app:30319`(Gitea 1.27.3,
|
||||
API 前缀 `/api/v1`;偶发 broken pipe / 超时,重试即可)。
|
||||
- **Git over SSH**:`ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/<Repo>.git`
|
||||
(各仓 origin 与 gitlink 均为此格式)。
|
||||
- **API token**:`~/.secrets/gitea`,两行键值 `URL_PREFIX=…` 与
|
||||
`ACCESS_TOKEN=<40 位 hex>`;取值 `grep ACCESS_TOKEN ~/.secrets/gitea | cut -d= -f2-`,
|
||||
用作 `Authorization: token <值>` 请求头。**token 值不入仓、不回显、不写进产出物**
|
||||
(baseline 硬边界 2:只记录来源、字段与可用状态)。
|
||||
- **SSH key**:`~/.ssh/home_gitea`(ED25519,公钥指纹
|
||||
`SHA256:GYXaOXIMeloQ1fKSfdJ/ndgPQzs/Ix7Hk62ekfkfQ20`,Gitea 侧密钥名 GMKtec);
|
||||
`~/.ssh/config` 已有 `git-ssh.u1680832.nyat.app` 主机条目,克隆/推送直接可用。
|
||||
|
||||
Generated
+127
-205
@@ -18,7 +18,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.5"
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -1211,6 +1211,13 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -1323,39 +1330,40 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
|
||||
"integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
|
||||
"integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "3.2.7",
|
||||
"@vitest/utils": "3.2.7",
|
||||
"chai": "^5.2.0",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
"@vitest/spy": "4.1.11",
|
||||
"@vitest/utils": "4.1.11",
|
||||
"chai": "^6.2.2",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
|
||||
"integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
|
||||
"integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.2.7",
|
||||
"@vitest/spy": "4.1.11",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
@@ -1367,42 +1375,42 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
|
||||
"integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
|
||||
"integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^2.0.0"
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
|
||||
"integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
|
||||
"integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.2.7",
|
||||
"pathe": "^2.0.3",
|
||||
"strip-literal": "^3.0.0"
|
||||
"@vitest/utils": "4.1.11",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
|
||||
"integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
|
||||
"integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.7",
|
||||
"magic-string": "^0.30.17",
|
||||
"@vitest/pretty-format": "4.1.11",
|
||||
"@vitest/utils": "4.1.11",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1410,28 +1418,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
|
||||
"integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
|
||||
"integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyspy": "^4.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
|
||||
"integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
|
||||
"integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.7",
|
||||
"loupe": "^3.1.4",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
"@vitest/pretty-format": "4.1.11",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
@@ -1494,16 +1499,6 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001813",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001813.tgz",
|
||||
@@ -1526,32 +1521,15 @@
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
||||
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assertion-error": "^2.0.1",
|
||||
"check-error": "^2.1.1",
|
||||
"deep-eql": "^5.0.1",
|
||||
"loupe": "^3.1.0",
|
||||
"pathval": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
|
||||
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -1584,16 +1562,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.440",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.440.tgz",
|
||||
@@ -1602,9 +1570,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
|
||||
"integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1756,13 +1724,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
||||
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -1831,6 +1792,20 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
|
||||
"integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
@@ -1838,16 +1813,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathval": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
|
||||
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -2015,29 +1980,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/strip-literal": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
||||
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^9.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal/node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
|
||||
"integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2049,11 +1994,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
|
||||
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
@@ -2072,30 +2020,10 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
|
||||
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
|
||||
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyspy": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz",
|
||||
"integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz",
|
||||
"integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2222,89 +2150,80 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
|
||||
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"debug": "^4.4.1",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"pathe": "^2.0.3",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"bin": {
|
||||
"vite-node": "vite-node.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
|
||||
"integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
|
||||
"integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.7",
|
||||
"@vitest/mocker": "3.2.7",
|
||||
"@vitest/pretty-format": "^3.2.7",
|
||||
"@vitest/runner": "3.2.7",
|
||||
"@vitest/snapshot": "3.2.7",
|
||||
"@vitest/spy": "3.2.7",
|
||||
"@vitest/utils": "3.2.7",
|
||||
"chai": "^5.2.0",
|
||||
"debug": "^4.4.1",
|
||||
"expect-type": "^1.2.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"@vitest/expect": "4.1.11",
|
||||
"@vitest/mocker": "4.1.11",
|
||||
"@vitest/pretty-format": "4.1.11",
|
||||
"@vitest/runner": "4.1.11",
|
||||
"@vitest/snapshot": "4.1.11",
|
||||
"@vitest/spy": "4.1.11",
|
||||
"@vitest/utils": "4.1.11",
|
||||
"es-module-lexer": "^2.0.0",
|
||||
"expect-type": "^1.3.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.2",
|
||||
"std-env": "^3.9.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^4.0.0-rc.1",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^0.3.2",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"tinypool": "^1.1.1",
|
||||
"tinyrainbow": "^2.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
|
||||
"vite-node": "3.2.4",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.1.0",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.2.7",
|
||||
"@vitest/ui": "3.2.7",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.1.11",
|
||||
"@vitest/browser-preview": "4.1.11",
|
||||
"@vitest/browser-webdriverio": "4.1.11",
|
||||
"@vitest/coverage-istanbul": "4.1.11",
|
||||
"@vitest/coverage-v8": "4.1.11",
|
||||
"@vitest/ui": "4.1.11",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
"jsdom": "*",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/debug": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser": {
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-istanbul": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-v8": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
@@ -2315,6 +2234,9 @@
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -20,6 +20,6 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.5"
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { marked } from 'marked'
|
||||
import { api } from '../api'
|
||||
import { store, useStore, activeRun, type LiveState, type ContextObs } from '../store'
|
||||
import type { Message, Part, HITLCard } from '../contracts'
|
||||
import { UIRenderer, InlineConfirmCard, hasOpenConfirm } from '@ui'
|
||||
import { liveToMessage, showProcessInline } from '../live'
|
||||
import type { Message, Part, HITLCard, ActivityCard } from '../contracts'
|
||||
import { UIRenderer, InlineConfirmCard, hasOpenConfirm, CardAnchor } from '@ui'
|
||||
import { docBridge } from './Workbench'
|
||||
import { groupParts, artifactIcon } from '../messageparts'
|
||||
|
||||
@@ -54,8 +55,16 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) {
|
||||
case 'plan':
|
||||
return null // 计划在右栏 Plan 标签呈现
|
||||
case 'card': {
|
||||
// cardlink 互定位(D32):点击 ↗ 定位收件箱对应卡(从 data 中解析 card:<id>)。
|
||||
const m = /card:([A-Za-z0-9_]+)/.exec(JSON.stringify(part.card?.data ?? ''))
|
||||
// 卡流锚点(D39):cardId 非空 = submit_card 引用部件——中栏自足(可展开全文)+ 跳收件箱。
|
||||
if (part.card?.cardId) {
|
||||
const ref = part.card.cardId
|
||||
const card = findCard(ref)
|
||||
const bodyChars = card?.body?.reduce((n, node) => n + (node.markdown?.length ?? 0), 0)
|
||||
return <CardAnchor title={card?.title ?? part.card.kind} chars={bodyChars} card={card}
|
||||
onLocate={() => store.openInbox(ref)} docBridge={docBridge} />
|
||||
}
|
||||
// 插件内嵌卡片部件(cardlink 互定位,D32):点击 ↗ 定位收件箱对应卡(从 data 解析 card:<id>)。
|
||||
const m = JSON.stringify(part.card?.data ?? '').match(/card:([A-Za-z0-9_]+)/)
|
||||
return (
|
||||
<div className="ui-section card-link" title={m ? '点击在收件箱中定位' : undefined}
|
||||
onClick={() => m && store.openInbox(m[1])}>
|
||||
@@ -76,6 +85,12 @@ function findArtifact(id: string) {
|
||||
return artifactCache[id]
|
||||
}
|
||||
|
||||
// 收件箱卡片查询缓存(store.cards 的派生索引;卡流锚点取卡数据用)。
|
||||
let cardCache: Record<string, ActivityCard> = {}
|
||||
function findCard(id: string) {
|
||||
return cardCache[id]
|
||||
}
|
||||
|
||||
// 过程折叠:run 终态后把连续 process 部件折叠为摘要行(可展开回看)。
|
||||
function ProcessCollapse({ parts }: { parts: Part[] }) {
|
||||
const toolCount = parts.filter(p => p.type === 'tool_call').length
|
||||
@@ -93,6 +108,26 @@ function ProcessCollapse({ parts }: { parts: Part[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
// 长回复折叠阈值(CF0):终态后超长正文收成摘要行,点击展开全文;live 期间全文可见。
|
||||
const LONG_REPLY_CHARS = 800
|
||||
|
||||
function LongReplyCollapse({ text }: { text: string }) {
|
||||
// 摘句取压缩空白后的前 100 字;字数按 Markdown 源码长度计。
|
||||
const excerpt = text.replace(/\s+/g, ' ').trim().slice(0, 100)
|
||||
return (
|
||||
<details className="process-collapse long-reply">
|
||||
<summary>
|
||||
<span>📄 长回复</span>
|
||||
<span>{text.length} 字(按 Markdown 源码计)</span>
|
||||
{excerpt && <span style={{ color: 'var(--text-faint)' }}>{excerpt}…</span>}
|
||||
</summary>
|
||||
<div className="body">
|
||||
<div className="part-text" dangerouslySetInnerHTML={{ __html: marked.parse(text) }} />
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null }) {
|
||||
const autoCollapse = useStore(s => s.ui.autoCollapseProcess)
|
||||
const ctxObs = useStore(s => (msg.runId ? s.contextObs[msg.runId] : undefined))
|
||||
@@ -101,42 +136,39 @@ function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null
|
||||
// 工具调用显示在它真正发生的位置,不被抽到消息头部重排。
|
||||
const blocks = useMemo(() => groupParts(parts), [parts])
|
||||
|
||||
// 流式中:过程部件实时展开;run 终态后:按偏好折叠为摘要行(可回看)。
|
||||
const showProcessInline = live || !autoCollapse
|
||||
// 流式期间:工具/思考阶段过程实时展开;一旦进入 responding(最终回复开始流式)
|
||||
// 立即折叠过程,只显式显示回复(Cursor 式);工具恢复(acting)时重新展开。
|
||||
// run 终态后:按偏好折叠为摘要行(可回看)。
|
||||
const showProcess = showProcessInline(live ?? null, autoCollapse)
|
||||
const lastContentIdx = (() => {
|
||||
for (let i = blocks.length - 1; i >= 0; i--) if (blocks[i].kind === 'content') return i
|
||||
return -1
|
||||
})()
|
||||
return (
|
||||
<div className="msg assistant">
|
||||
<div className="msg assistant" data-msg-id={msg.id}>
|
||||
<div className="msg-role">
|
||||
<span className="role-mark role-assistant"><BrandMark size={16} /></span>
|
||||
<span>助手</span>
|
||||
{live && <span className="live-bar"><span className="live-dot" />{live.phase}</span>}
|
||||
</div>
|
||||
{blocks.map((b, i) => b.kind === 'process'
|
||||
? (showProcessInline
|
||||
? (showProcess
|
||||
? <div className="part-group" key={i}>{b.parts.map((p, j) => <div className="part" key={j}><PartView part={p} live={!!live} /></div>)}</div>
|
||||
: <ProcessCollapse key={i} parts={b.parts} />)
|
||||
: <div className="part" key={i}>
|
||||
<PartView part={b.part} live={!!(live && i === lastContentIdx && b.part.type === 'text')} />
|
||||
</div>)}
|
||||
: (() => {
|
||||
// CF0:非 live 的超长文本部件折叠为摘要行;live 与短文本不受影响。
|
||||
if (!live && b.part.type === 'text' && (b.part.text?.length ?? 0) > LONG_REPLY_CHARS)
|
||||
return <LongReplyCollapse key={i} text={b.part.text ?? ''} />
|
||||
return <div className="part" key={i}>
|
||||
<PartView part={b.part} live={!!(live && i === lastContentIdx && b.part.type === 'text')} />
|
||||
</div>
|
||||
})())}
|
||||
{live && <LiveAgentNotes live={live} />}
|
||||
{!live && ctxObs && <ContextObsView obs={ctxObs} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// live 投影:run 进行中没有已落库的助手消息,把 LiveState 合成为同构的临时消息。
|
||||
// LiveState.parts 已按事件到达序维护(store.applyEvent),直接透传即保序。
|
||||
function liveToMessage(live: LiveState): Message {
|
||||
if (live.parts.length === 0) {
|
||||
// 兜底(无事件到达的短暂窗口):只给 phase 占位,避免空白。
|
||||
return { id: `live:${live.runId}`, conversationId: '', role: 'assistant', runId: live.runId, parts: [], createdAt: '' }
|
||||
}
|
||||
return { id: `live:${live.runId}`, conversationId: '', role: 'assistant', runId: live.runId, parts: live.parts, createdAt: '' }
|
||||
}
|
||||
|
||||
// 上下文组装观测(4.10/4.17):模型实际看到什么、裁了什么——可解释性入口。
|
||||
function ContextObsView({ obs }: { obs: ContextObs }) {
|
||||
return (
|
||||
@@ -243,12 +275,13 @@ export function Conversation() {
|
||||
const bodyRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
artifactCache = Object.fromEntries(artifacts.map(a => [a.id, a]))
|
||||
cardCache = Object.fromEntries(cards.map(c => [c.id, c]))
|
||||
|
||||
const active = activeRun(runs)
|
||||
|
||||
useEffect(() => {
|
||||
bodyRef.current?.scrollTo({ top: bodyRef.current.scrollHeight })
|
||||
}, [messages.length, live?.text, live?.thinking, live?.toolCalls.length, live?.phase, activeHitl.length])
|
||||
}, [messages.length, live?.items, live?.phase, activeHitl.length])
|
||||
|
||||
const send = async () => {
|
||||
if (!text.trim() && attachmentIds.length === 0) return
|
||||
@@ -308,7 +341,7 @@ export function Conversation() {
|
||||
// 内联确认卡(T3):锚定本消息的危险命令确认,双现于对话流与收件箱。
|
||||
const anchored = cards.filter(c => c.anchorMessageId === m.id && c.kind === 'confirm')
|
||||
return (
|
||||
<div className="msg user" key={m.id}>
|
||||
<div className="msg user" key={m.id} data-msg-id={m.id}>
|
||||
<div className="msg-role">
|
||||
<span className="role-mark"><Avatar name={myName} size="sm" /></span>你
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function Workbench() {
|
||||
onAction: (c: ActivityCard, id: string) => store.cardAction(c, id),
|
||||
onPin: (c: ActivityCard, p: boolean) => store.cardPin(c, p),
|
||||
onDismiss: (c: ActivityCard) => store.cardDismiss(c),
|
||||
onLocate: (c: ActivityCard) => store.openInbox(c.id),
|
||||
onLocate: (c: ActivityCard) => store.locateMessage(c.anchorMessageId),
|
||||
}} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ export interface Part {
|
||||
artifact?: { artifactId: ID; name: string; kind?: string };
|
||||
hitl?: { cardId: ID; question: string; resolved: boolean; answer?: string };
|
||||
plan?: PlanSnapshot;
|
||||
card?: { kind: string; data: UINode[] };
|
||||
card?: { kind: string; data: UINode[]; cardId?: ID }; // cardId 非空 = 收件箱卡片引用部件(submit_card)
|
||||
error?: { code: string; message: string };
|
||||
process?: boolean; // run 终态后折叠为摘要行
|
||||
}
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// live 流式投影纯逻辑(中栏过程/回复分离):事件按到达序并入 items——thinking/text
|
||||
// 增量各并入其末段,工具按当前位置落位。SSE 有序且后端每轮按 思考→正文→工具 追加落库
|
||||
// 部件,事件到达序即落库序,故合成消息与落库部件同构,终态对账不重排。
|
||||
// 文本段是否为过程(process)由「本段之后、下一个思考/文本段之前是否出现工具」判定,
|
||||
// 对齐落库的 Process: len(calls) > 0——工具轮解说的 tool.call 一到即并入过程区,
|
||||
// 无需等下一轮文本段开启。
|
||||
// 残余边界:某轮若既无正文也无思考、仅静默发起工具,其调用会被并入上一文本段(需协议
|
||||
// 携带轮次归属才能消除;现行事件流无法区分)。
|
||||
import type { Message, Part } from './contracts'
|
||||
import type { LiveState } from './store'
|
||||
|
||||
// 到达序部件流的元素。
|
||||
export type LiveItem =
|
||||
| { kind: 'thinking'; text: string }
|
||||
| { kind: 'text'; index: number; text: string }
|
||||
| { kind: 'tool'; callId?: string; name: string; status?: string; summary?: string }
|
||||
| { kind: 'artifact'; artifact: { artifactId: string; name: string; kind?: string } }
|
||||
|
||||
// applyThinkingDelta 并入思考增量:末段是思考则追加,否则开启新思考段(新轮开始)。
|
||||
export function applyThinkingDelta(items: LiveItem[], delta: string): LiveItem[] {
|
||||
const last = items[items.length - 1]
|
||||
if (last && last.kind === 'thinking') {
|
||||
const out = items.slice()
|
||||
out[out.length - 1] = { kind: 'thinking', text: last.text + delta }
|
||||
return out
|
||||
}
|
||||
return [...items, { kind: 'thinking', text: delta }]
|
||||
}
|
||||
|
||||
// applyTextDelta 并入正文增量:末段是同 index 的文本段则追加,否则开启新文本段。
|
||||
export function applyTextDelta(items: LiveItem[], index: number, delta: string): LiveItem[] {
|
||||
const last = items[items.length - 1]
|
||||
if (last && last.kind === 'text' && last.index === index) {
|
||||
const out = items.slice()
|
||||
out[out.length - 1] = { kind: 'text', index, text: last.text + delta }
|
||||
return out
|
||||
}
|
||||
return [...items, { kind: 'text', index, text: delta }]
|
||||
}
|
||||
|
||||
export function applyToolCall(items: LiveItem[], name: string, callId?: string): LiveItem[] {
|
||||
return [...items, { kind: 'tool', callId, name }]
|
||||
}
|
||||
|
||||
// applyArtifactCreated 追加工件引用(artifact.created 事件;与落库一致标记为过程)。
|
||||
export function applyArtifactCreated(items: LiveItem[], artifact: { artifactId: string; name: string; kind?: string }): LiveItem[] {
|
||||
return [...items, { kind: 'artifact', artifact }]
|
||||
}
|
||||
|
||||
// applyToolResult 回填对应工具(优先按 callId 配对,回落到最近一个未决工具——
|
||||
// 与落库的尾部配对语义一致)。
|
||||
export function applyToolResult(items: LiveItem[], callId: string | undefined, status: string, summary?: string): LiveItem[] {
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
const it = items[i]
|
||||
if (it.kind !== 'tool' || it.status !== undefined) continue
|
||||
if (callId && it.callId && it.callId !== callId) continue
|
||||
const out = items.slice()
|
||||
out[i] = { kind: 'tool', callId: it.callId, name: it.name, status, summary }
|
||||
return out
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// liveToMessage:run 进行中没有已落库的助手消息,把 LiveState 合成同构临时消息,
|
||||
// 复用 AssistantMessage 渲染(思考/工具/文本实时上屏);终态对账后由真消息替代。
|
||||
export function liveToMessage(live: LiveState): Message {
|
||||
// 文本段是否为过程:本段之后、下一个思考/文本段之前出现工具(= 该轮带工具调用)。
|
||||
const textProcess = new Map<number, boolean>()
|
||||
for (let i = 0; i < live.items.length; i++) {
|
||||
if (live.items[i].kind !== 'text') continue
|
||||
let hasCalls = false
|
||||
for (let j = i + 1; j < live.items.length && live.items[j].kind === 'tool'; j++) hasCalls = true
|
||||
textProcess.set(i, hasCalls)
|
||||
}
|
||||
const parts: Part[] = []
|
||||
for (let i = 0; i < live.items.length; i++) {
|
||||
const it = live.items[i]
|
||||
if (it.kind === 'thinking') {
|
||||
if (it.text) parts.push({ type: 'thinking', text: it.text, process: true })
|
||||
} else if (it.kind === 'tool') {
|
||||
parts.push({ type: 'tool_call', process: true, toolCall: { id: it.callId ?? '', name: it.name } })
|
||||
if (it.status !== undefined) {
|
||||
parts.push({ type: 'tool_result', process: true, toolCall: { id: it.callId ?? '', name: it.name },
|
||||
toolResult: { callId: it.callId ?? '', status: it.status === 'error' ? 'error' : 'ok', summary: it.summary } })
|
||||
}
|
||||
} else if (it.kind === 'artifact') {
|
||||
parts.push({ type: 'artifact_ref', process: true, artifact: it.artifact })
|
||||
} else {
|
||||
parts.push({ type: 'text', text: it.text, process: textProcess.get(i) || undefined })
|
||||
}
|
||||
}
|
||||
return { id: `live:${live.runId}`, conversationId: '', role: 'assistant', runId: live.runId, parts, createdAt: '' }
|
||||
}
|
||||
|
||||
// showProcessInline:流式期间过程区是否展开——thinking/acting 实时展开;一旦进入
|
||||
// responding(最终回复开始流式)立即折叠,只显示回复(Cursor 式),工具恢复再展开。
|
||||
// 非 live 消息随「自动折叠过程」偏好。
|
||||
export function showProcessInline(live: LiveState | null, autoCollapse: boolean): boolean {
|
||||
return live ? live.phase !== 'responding' : !autoCollapse
|
||||
}
|
||||
+20
-42
@@ -2,9 +2,10 @@
|
||||
// useSyncExternalStore 驱动;live 流式部件为会话内临时态,run 终态后由消息树对账收敛。
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { api, connectEvents } from './api'
|
||||
import { applyTextDelta, applyThinkingDelta, applyToolCall, applyToolResult, applyArtifactCreated, type LiveItem } from './live'
|
||||
import type {
|
||||
Project, Conversation, Message, Run, Artifact, HITLCard, PlanSnapshot, TodoList,
|
||||
AgentProfile, StreamEvent, PluginPanel, ActivityCard, Part,
|
||||
AgentProfile, StreamEvent, PluginPanel, ActivityCard,
|
||||
} from './contracts'
|
||||
|
||||
export interface ContextObs {
|
||||
@@ -16,13 +17,11 @@ export interface ContextObs {
|
||||
export interface LiveState {
|
||||
runId: string
|
||||
phase: string
|
||||
thinking: string
|
||||
text: string
|
||||
toolCalls: { name: string; status?: string; summary?: string }[]
|
||||
// 到达序部件流(见 live.ts LiveItem):事件到达序即落库序,与落库部件的轮次穿插序
|
||||
// 同构;文本段是否为过程由「本段内是否出现工具调用」判定,tool.call 一到即改标。
|
||||
items: LiveItem[]
|
||||
agentNotes: Record<string, string>
|
||||
startedAt: number
|
||||
/** 到达序部件(渲染顺序 = 事件顺序;与后端落库的交错顺序同构)。 */
|
||||
parts: Part[]
|
||||
}
|
||||
|
||||
export interface UIState {
|
||||
@@ -264,48 +263,22 @@ export const store = {
|
||||
switch (e.type) {
|
||||
case 'message.delta': {
|
||||
const p = e.payload as { index: number; text: string }
|
||||
set({ live: touchLive(e.runId ?? '', l => {
|
||||
l.text += p.text
|
||||
// 到达序:文本增量并入尾部的流式 text part,否则新开一段。
|
||||
const last = l.parts[l.parts.length - 1]
|
||||
if (last && last.type === 'text' && !last.process) last.text = (last.text ?? '') + p.text
|
||||
else l.parts.push({ type: 'text', text: p.text })
|
||||
}) })
|
||||
set({ live: touchLive(e.runId ?? '', l => { l.items = applyTextDelta(l.items, p.index, p.text) }) })
|
||||
break
|
||||
}
|
||||
case 'thinking.delta': {
|
||||
const p = e.payload as { index: number; text: string }
|
||||
set({ live: touchLive(e.runId ?? '', l => {
|
||||
l.thinking += p.text
|
||||
const last = l.parts[l.parts.length - 1]
|
||||
if (last && last.type === 'thinking' && last.process) last.text = (last.text ?? '') + p.text
|
||||
else l.parts.push({ type: 'thinking', text: p.text, process: true })
|
||||
}) })
|
||||
set({ live: touchLive(e.runId ?? '', l => { l.items = applyThinkingDelta(l.items, p.text) }) })
|
||||
break
|
||||
}
|
||||
case 'tool.call': {
|
||||
const p = e.payload as { callId: string; name: string }
|
||||
set({ live: touchLive(e.runId ?? '', l => {
|
||||
l.toolCalls.push({ name: p.name })
|
||||
l.parts.push({ type: 'tool_call', process: true, toolCall: { id: p.callId, name: p.name } })
|
||||
}) })
|
||||
set({ live: touchLive(e.runId ?? '', l => { l.items = applyToolCall(l.items, p.name, p.callId) }) })
|
||||
break
|
||||
}
|
||||
case 'tool.result': {
|
||||
const p = e.payload as { callId: string; status: string; summary?: string }
|
||||
set({ live: touchLive(e.runId ?? '', l => {
|
||||
let name = ''
|
||||
for (let i = l.toolCalls.length - 1; i >= 0; i--) {
|
||||
if (l.toolCalls[i].status === undefined) {
|
||||
l.toolCalls[i] = { ...l.toolCalls[i], status: p.status, summary: p.summary }
|
||||
name = l.toolCalls[i].name
|
||||
break
|
||||
}
|
||||
}
|
||||
// 结果紧跟对应 tool_call 之后展示(顺序执行下 push 即到位)。
|
||||
l.parts.push({ type: 'tool_result', process: true, toolCall: { id: p.callId, name },
|
||||
toolResult: { callId: p.callId, status: p.status === 'error' ? 'error' : 'ok', summary: p.summary } })
|
||||
}) })
|
||||
set({ live: touchLive(e.runId ?? '', l => { l.items = applyToolResult(l.items, p.callId, p.status, p.summary) }) })
|
||||
break
|
||||
}
|
||||
case 'run.phase': {
|
||||
@@ -369,7 +342,7 @@ export const store = {
|
||||
const p = e.payload as { artifact?: { id: string; name: string; kind?: string } }
|
||||
if (p?.artifact) {
|
||||
set({ live: touchLive(e.runId ?? '', l => {
|
||||
l.parts.push({ type: 'artifact_ref', artifact: { artifactId: p.artifact!.id, name: p.artifact!.name, kind: p.artifact!.kind } })
|
||||
l.items = applyArtifactCreated(l.items, { artifactId: p.artifact!.id, name: p.artifact!.name, kind: p.artifact!.kind })
|
||||
}) })
|
||||
}
|
||||
if (state.currentProjectId) store.refreshArtifacts(state.currentProjectId)
|
||||
@@ -406,7 +379,7 @@ export const store = {
|
||||
// 竞态保护:POST 返回前 SSE 事件可能已先行到达并累积(同 run),
|
||||
// 此时保留已累积的 live,只补挂用户消息;否则初始化空 live。
|
||||
if (!state.live || state.live.runId !== out.run.id) {
|
||||
set({ live: { runId: out.run.id, phase: 'thinking', thinking: '', text: '', toolCalls: [], agentNotes: {}, startedAt: Date.now(), parts: [] } })
|
||||
set({ live: { runId: out.run.id, phase: 'thinking', items: [], agentNotes: {}, startedAt: Date.now() } })
|
||||
}
|
||||
set({ messages: [...state.messages, out.message] })
|
||||
store.connectIfNeeded()
|
||||
@@ -530,6 +503,13 @@ export const store = {
|
||||
document.querySelector(`[data-card-id="${cardId}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}, 60)
|
||||
},
|
||||
// 卡→消息互定位另一半(D32):从收件箱卡滚回产生它的对话消息。
|
||||
locateMessage(messageId?: string) {
|
||||
if (!messageId) return
|
||||
setTimeout(() => {
|
||||
document.querySelector(`[data-msg-id="${messageId}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}, 60)
|
||||
},
|
||||
openTracker(runId: string) { setUI({ trackerRunId: runId }) },
|
||||
viewArtifact(a: Artifact | null) { setUI({ viewingArtifact: a }) },
|
||||
openSettings(open: boolean) { setUI({ settingsOpen: open }) },
|
||||
@@ -541,10 +521,8 @@ export const store = {
|
||||
}
|
||||
|
||||
function touchLive(_runId: string, mutate: (l: LiveState) => void): LiveState | null {
|
||||
const base = state.live ?? { runId: _runId, phase: 'thinking', thinking: '', text: '', toolCalls: [], agentNotes: {}, startedAt: Date.now(), parts: [] as Part[] }
|
||||
const next: LiveState = { ...base, toolCalls: [...base.toolCalls], agentNotes: { ...base.agentNotes }, parts: [...base.parts] }
|
||||
// parts 内部对象的后续可变更新(delta 并入尾部)需要新引用——浅拷贝后再克隆被改的尾部。
|
||||
if (next.parts.length > 0) next.parts[next.parts.length - 1] = { ...next.parts[next.parts.length - 1] }
|
||||
const base = state.live ?? { runId: _runId, phase: 'thinking', items: [], agentNotes: {}, startedAt: Date.now() }
|
||||
const next: LiveState = { ...base, agentNotes: { ...base.agentNotes } }
|
||||
mutate(next)
|
||||
return next
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// live 流式投影纯逻辑测试(中栏过程/回复分离):到达序并段、process 标记时机、
|
||||
// 轮次穿插同构、过程区折叠谓词。
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
applyTextDelta, applyThinkingDelta, applyToolCall, applyToolResult, applyArtifactCreated,
|
||||
liveToMessage, showProcessInline, type LiveItem,
|
||||
} from '../src/live'
|
||||
import type { LiveState } from '../src/store'
|
||||
|
||||
function live(over: Partial<LiveState> = {}): LiveState {
|
||||
return { runId: 'r1', phase: 'thinking', items: [], agentNotes: {}, startedAt: 0, ...over }
|
||||
}
|
||||
|
||||
// 两轮流式事件回放:思考→解说文本→工具→思考→最终回复(与后端事件序一致)。
|
||||
function twoTurnItems(): LiveItem[] {
|
||||
let items: LiveItem[] = []
|
||||
items = applyThinkingDelta(items, '先想想')
|
||||
items = applyThinkingDelta(items, '一下')
|
||||
items = applyTextDelta(items, 0, '我先更新待办。')
|
||||
items = applyToolCall(items, 'update_todos', 'c1')
|
||||
items = applyToolResult(items, 'c1', 'ok', '已更新')
|
||||
items = applyThinkingDelta(items, '再想想')
|
||||
items = applyTextDelta(items, 1, '最终')
|
||||
items = applyTextDelta(items, 1, '回复')
|
||||
return items
|
||||
}
|
||||
|
||||
describe('applyThinkingDelta / applyTextDelta', () => {
|
||||
it('merges thinking deltas into the last thinking segment', () => {
|
||||
let items: LiveItem[] = []
|
||||
items = applyThinkingDelta(items, '先')
|
||||
items = applyThinkingDelta(items, '想想')
|
||||
expect(items).toEqual([{ kind: 'thinking', text: '先想想' }])
|
||||
})
|
||||
it('opens a new thinking segment after text/tool (turn boundary)', () => {
|
||||
let items: LiveItem[] = applyThinkingDelta([], '第一轮思考')
|
||||
items = applyTextDelta(items, 0, '解说')
|
||||
items = applyThinkingDelta(items, '第二轮思考')
|
||||
expect(items).toEqual([
|
||||
{ kind: 'thinking', text: '第一轮思考' },
|
||||
{ kind: 'text', index: 0, text: '解说' },
|
||||
{ kind: 'thinking', text: '第二轮思考' },
|
||||
])
|
||||
})
|
||||
it('appends text to the last segment when index matches', () => {
|
||||
let items: LiveItem[] = []
|
||||
items = applyTextDelta(items, 0, '先查')
|
||||
items = applyTextDelta(items, 0, '一下')
|
||||
expect(items).toEqual([{ kind: 'text', index: 0, text: '先查一下' }])
|
||||
})
|
||||
it('opens a new text segment when index advances (new turn)', () => {
|
||||
let items: LiveItem[] = applyTextDelta([], 0, '解说')
|
||||
items = applyTextDelta(items, 1, '最终')
|
||||
items = applyTextDelta(items, 1, '回复')
|
||||
expect(items).toEqual([
|
||||
{ kind: 'text', index: 0, text: '解说' },
|
||||
{ kind: 'text', index: 1, text: '最终回复' },
|
||||
])
|
||||
})
|
||||
it('fills the most recent pending tool call (callId match, tail fallback)', () => {
|
||||
let items: LiveItem[] = []
|
||||
items = applyToolCall(items, 'a', 'ca')
|
||||
items = applyToolCall(items, 'b', 'cb')
|
||||
items = applyToolResult(items, 'cb', 'ok', '结果')
|
||||
expect(items).toEqual([
|
||||
{ kind: 'tool', callId: 'ca', name: 'a' },
|
||||
{ kind: 'tool', callId: 'cb', name: 'b', status: 'ok', summary: '结果' },
|
||||
])
|
||||
// 无 callId 的事件回落到尾部配对。
|
||||
items = applyToolResult(items, undefined, 'ok')
|
||||
expect(items[0]).toEqual({ kind: 'tool', callId: 'ca', name: 'a', status: 'ok' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('liveToMessage', () => {
|
||||
it('interleaves parts per turn in arrival order, matching persisted parts', () => {
|
||||
const msg = liveToMessage(live({ items: twoTurnItems() }))
|
||||
expect(msg.parts).toEqual([
|
||||
{ type: 'thinking', text: '先想想一下', process: true },
|
||||
{ type: 'text', text: '我先更新待办。', process: true },
|
||||
{ type: 'tool_call', process: true, toolCall: { id: 'c1', name: 'update_todos' } },
|
||||
{ type: 'tool_result', process: true, toolCall: { id: 'c1', name: 'update_todos' },
|
||||
toolResult: { callId: 'c1', status: 'ok', summary: '已更新' } },
|
||||
{ type: 'thinking', text: '再想想', process: true },
|
||||
{ type: 'text', text: '最终回复' },
|
||||
])
|
||||
})
|
||||
it('marks the in-flight segment as process the moment its tool call arrives', () => {
|
||||
let items: LiveItem[] = []
|
||||
items = applyTextDelta(items, 0, '我先查一下')
|
||||
const before = liveToMessage(live({ items }))
|
||||
expect(before.parts.map(p => p.process)).toEqual([undefined]) // 尚无工具:仍在回复位
|
||||
items = applyToolCall(items, 'web_search')
|
||||
const after = liveToMessage(live({ items }))
|
||||
expect(after.parts.map(p => p.process)).toEqual([true, true]) // tool.call 一到即改标
|
||||
})
|
||||
it('single final segment without tools carries no process flag', () => {
|
||||
let items: LiveItem[] = []
|
||||
items = applyTextDelta(items, 0, '直接回答')
|
||||
const msg = liveToMessage(live({ items }))
|
||||
expect(msg.parts.map(p => p.process)).toEqual([undefined])
|
||||
})
|
||||
it('tools between segments keep every non-final segment in the process area', () => {
|
||||
let items: LiveItem[] = []
|
||||
items = applyTextDelta(items, 0, '第一轮')
|
||||
items = applyToolCall(items, 'a')
|
||||
items = applyToolResult(items, undefined, 'ok')
|
||||
items = applyTextDelta(items, 1, '第二轮')
|
||||
items = applyToolCall(items, 'b')
|
||||
items = applyTextDelta(items, 2, '最终回复')
|
||||
const msg = liveToMessage(live({ items }))
|
||||
expect(msg.parts.filter(p => p.type === 'text').map(p => p.process)).toEqual([true, true, undefined])
|
||||
// 顺序必须与落库穿插序一致:text → tool → text → tool → text。
|
||||
expect(msg.parts.map(p => p.type)).toEqual(['text', 'tool_call', 'tool_result', 'text', 'tool_call', 'text'])
|
||||
})
|
||||
it('thinking and tool parts are always process', () => {
|
||||
let items: LiveItem[] = []
|
||||
items = applyThinkingDelta(items, '想')
|
||||
items = applyTextDelta(items, 0, '答')
|
||||
items = applyToolCall(items, 'web_search')
|
||||
items = applyToolResult(items, undefined, 'ok', '命中')
|
||||
const msg = liveToMessage(live({ items }))
|
||||
for (const p of msg.parts.filter(p => p.type !== 'text')) expect(p.process).toBe(true)
|
||||
})
|
||||
it('artifact refs land after their tool call and are process', () => {
|
||||
let items: LiveItem[] = []
|
||||
items = applyTextDelta(items, 0, '生成报表。')
|
||||
items = applyToolCall(items, 'make_report', 'c1')
|
||||
items = applyToolResult(items, 'c1', 'ok')
|
||||
items = applyArtifactCreated(items, { artifactId: 'a1', name: 'report.csv', kind: 'csv' })
|
||||
items = applyTextDelta(items, 1, '完成。')
|
||||
const msg = liveToMessage(live({ items }))
|
||||
expect(msg.parts.map(p => p.type)).toEqual(['text', 'tool_call', 'tool_result', 'artifact_ref', 'text'])
|
||||
expect(msg.parts.find(p => p.type === 'artifact_ref')).toMatchObject({ process: true })
|
||||
expect(msg.parts.filter(p => p.type === 'text').map(p => p.process)).toEqual([true, undefined])
|
||||
})
|
||||
})
|
||||
|
||||
describe('showProcessInline', () => {
|
||||
it('expands during thinking/acting and collapses the moment responding starts', () => {
|
||||
expect(showProcessInline(live({ phase: 'thinking' }), true)).toBe(true)
|
||||
expect(showProcessInline(live({ phase: 'acting' }), true)).toBe(true)
|
||||
expect(showProcessInline(live({ phase: 'responding' }), true)).toBe(false)
|
||||
})
|
||||
it('falls back to the auto-collapse preference when no live run', () => {
|
||||
expect(showProcessInline(null, true)).toBe(false)
|
||||
expect(showProcessInline(null, false)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/main.tsx","./src/messageparts.ts","./src/passwords.ts","./src/store.ts","./src/tasklist.ts","./src/components/AdminUsersModal.tsx","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/LoginPage.tsx","./src/components/SettingsModal.tsx","./src/components/TaskSidebar.tsx","./src/components/Workbench.tsx","./src/components/Workspaces.tsx","./test/auth.test.ts","./test/contracts.test.ts","./test/messageparts.test.ts","./test/tasklist.test.ts"],"version":"5.9.3"}
|
||||
{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/live.ts","./src/main.tsx","./src/messageparts.ts","./src/passwords.ts","./src/store.ts","./src/tasklist.ts","./src/components/AdminUsersModal.tsx","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/LoginPage.tsx","./src/components/ProjectSettingsModal.tsx","./src/components/SettingsModal.tsx","./src/components/TaskSidebar.tsx","./src/components/Workbench.tsx","./src/components/Workspaces.tsx","./test/auth.test.ts","./test/contracts.test.ts","./test/live.test.ts","./test/messageparts.test.ts","./test/tasklist.test.ts"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user