@xiaosuanli/dsh-plugin 0.0.0-stage → 1.0.0
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 +69 -2
- package/client.js +308 -0
- package/cordis.patch.yml +3 -0
- package/host.mjs +183 -0
- package/package.json +68 -4
- package/skills/xiaosuanli-director/SKILL.md +26 -0
- package/skills/xiaosuanli-director/references/director-workflow.md +85 -0
- package/skills/xiaosuanli-director/references/import-and-remake.md +19 -0
- package/skills/xiaosuanli-director/references/install.md +11 -0
- package/src/connect/http.mjs +48 -0
- package/src/connect/index.mjs +231 -0
- package/src/connect/operations.mjs +11 -0
package/README.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 小算狸创作 · DeepSeek Harness 插件
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
在 DeepSeek Harness 中连接小算狸账号,选择导演工程与章节,通过对话编辑剧情、制作素材图和视频,并在小算狸导演台网页继续图形化编辑。
|
|
4
|
+
|
|
5
|
+
插件使用你在 DSH 中选择的模型。图片与视频任务由小算狸执行,使用你的账号权限与额度;安装插件本身不提交生成任务。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
需要 Node.js ≥22.19.0,以及 DeepSeek Harness `0.2.1-rc.2`。当前仅针对该预览版本维护兼容性。
|
|
10
|
+
|
|
11
|
+
从 npm 安装 1.0.0:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
dsh plugin --profile web add @xiaosuanli/dsh-plugin@1.0.0
|
|
15
|
+
dsh web
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
也可从 GitHub 安装已验收源码:`dsh plugin --profile web add github:xiaosuanli/dsh-creative#master`。npm 包名沿用 `@xiaosuanli/dsh-plugin`,代码仓库名为 `dsh-creative`。
|
|
19
|
+
|
|
20
|
+
已经安装同名开发包时,在同一个 profile 更新该包并重启 Host。无需创建第二份配置行;包名、客户端模块和账号凭据继续使用原有命名空间。
|
|
21
|
+
|
|
22
|
+
## 首次使用
|
|
23
|
+
|
|
24
|
+
1. 点击右下角“小算狸”悬浮按钮,或打开设置中的“小算狸”。
|
|
25
|
+
2. 点击“登录并连接小算狸”,在浏览器完成账号登录和授权,将一次性授权码填回插件面板。
|
|
26
|
+
3. 选择工程、选择章节,点击新建聊天;也可以在聊天中让助手列出工程并打开章节。
|
|
27
|
+
4. 说明故事、画风和目标时长。助手按实际工具权限执行,并返回进度和导演台编辑链接。
|
|
28
|
+
|
|
29
|
+
没有工程时,在未绑定章节的新会话中发送:“使用小算狸导演 Skill,创建一个叫《我的第一部短片》的工程,并打开第一章。”权限或服务能力不足时,助手会给出具体原因及网页入口。
|
|
30
|
+
|
|
31
|
+
一个对话固定一个章节;需要另一章时新建对话。同章可以有多个对话,新的真实用户消息接管操作权,打开面板或查询状态不会接管。长期令牌保存在 Host 凭据服务,一次性授权码填写在插件面板。
|
|
32
|
+
|
|
33
|
+
## 能力与服务依赖
|
|
34
|
+
|
|
35
|
+
- 工程与章节发现、创建、打开,以及账号、会员和积分查询。
|
|
36
|
+
- 小说/文章解析,剧情与分镜编辑,人物/场景/物品素材制作。
|
|
37
|
+
- 上传视频的解析与复刻、视频生成、任务恢复、结果选择与成片导出。
|
|
38
|
+
|
|
39
|
+
实际可用工具及参数由服务端授权和 capabilities 决定。每章文本输入最多 20,000 字,复刻源视频接受 15~180 秒;目标成片时长按具体任务规划。详见 [导演 skill](skills/xiaosuanli-director/SKILL.md)。
|
|
40
|
+
|
|
41
|
+
默认连接小算狸正式站点。插件依赖小算狸的公开账号授权、账户、工程/章节和导演工具接口;本仓迁移与本地安装通过,不代表所有服务端功能已经上线验收。
|
|
42
|
+
|
|
43
|
+
## 开发与打包
|
|
44
|
+
|
|
45
|
+
源码为可直接运行的 JavaScript,无需编译或安装时构建脚本:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npm ci --ignore-scripts
|
|
49
|
+
npm test
|
|
50
|
+
npm pack --pack-destination /absolute/path/to/artifacts
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
将生成的 `.tgz` 交给 `dsh plugin --profile web add /absolute/path/to/package.tgz` 安装。发布包只包含 Host、Client、Connect、skill 和安装说明;测试、开发文档与运行凭据不进入安装包。
|
|
54
|
+
|
|
55
|
+
本地验证在独立的 DSH home/profile 执行,不使用现有账号。服务地址通过受信任的插件配置设置:
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
- insert:
|
|
59
|
+
- id: xiaosuanli-connect
|
|
60
|
+
name: '@xiaosuanli/dsh-plugin'
|
|
61
|
+
config:
|
|
62
|
+
siteOrigin: https://www.xiaosuanli.com
|
|
63
|
+
apiOrigin: https://api.xiaosuanli.com
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
更新既有配置行时按 Harness 的 patch 规则操作。不同环境授权隔离,使用目标环境的账号与服务权限。
|
|
67
|
+
|
|
68
|
+
自动发布:复制 `.env.example` 为本地 `.env` 并设置 `NPM_TOKEN`,提交并验证 test、合入及推送 master 后执行 `npm run release -- --artifacts-dir /absolute/path/to/artifacts`。详见 [发布说明](docs/releasing.md)。
|
|
69
|
+
|
|
70
|
+
维护与迁移记录见 [文档索引](docs/README.md)。
|
package/client.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@xiaosuanli/dsh-plugin',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react');
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
// Official brand artwork, resized for the bundled UI.
|
|
7
|
+
const brandLogo = "data:image/png;base64,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";
|
|
8
|
+
const styles = `
|
|
9
|
+
.xsl-dir { --xsl-text: var(--dsw-alias-label-primary, #eee); --xsl-muted: var(--dsw-alias-label-tertiary, #929292); --xsl-line: var(--dsw-alias-border-l2, #383838); --xsl-surface: var(--dsw-alias-interactive-bg-hover, #242424); --xsl-accent: var(--dsw-alias-brand-primary, #597ef7); color: var(--xsl-text); font-family: var(--dsw-font-family, inherit); font-size: 13px; line-height: 1.55; box-sizing: border-box; }
|
|
10
|
+
.xsl-dir *, .xsl-dir *::before, .xsl-dir *::after { box-sizing: border-box; }
|
|
11
|
+
.xsl-dir h2, .xsl-dir h3, .xsl-dir p { margin: 0; }
|
|
12
|
+
.xsl-dir button, .xsl-dir input, .xsl-dir select { font: inherit; }
|
|
13
|
+
.xsl-dir button, .xsl-dir a, .xsl-dir input, .xsl-dir select, .xsl-dir summary { -webkit-app-region: no-drag; }
|
|
14
|
+
.xsl-dir button { cursor: pointer; }
|
|
15
|
+
.xsl-dir button:disabled { cursor: default; opacity: .42; }
|
|
16
|
+
.xsl-dir button:focus-visible, .xsl-dir a:focus-visible, .xsl-dir summary:focus-visible, .xsl-dir input:focus-visible, .xsl-dir select:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--xsl-accent)); outline-offset: 3px; }
|
|
17
|
+
.xsl-dir svg { display: block; flex: none; }
|
|
18
|
+
.xsl-panel { width: 100%; height: 100%; min-width: 0; min-height: 0; overflow-y: auto; scrollbar-width: thin; padding: 20px 18px 18px; }
|
|
19
|
+
.xsl-floating { position: absolute; right: 22px; bottom: 22px; z-index: 1; pointer-events: auto; }
|
|
20
|
+
.xsl-launcher { display: flex; align-items: center; justify-content: center; gap: 8px; height: 44px; padding: 0 16px 0 12px; margin-left: auto; border: 1px solid #91b1c42e; border-radius: 24px; background: color-mix(in srgb, var(--dsw-alias-bg-base, #202020) 94%, #b8d6ef); color: var(--xsl-text); box-shadow: 0 3px 14px #3b66851a; transition: border-color .15s, box-shadow .15s; }
|
|
21
|
+
.xsl-launcher > svg { color: #d99b62; }
|
|
22
|
+
.xsl-launcher:hover { border-color: #7ba4c47a; box-shadow: 0 5px 18px #3b668524; }
|
|
23
|
+
.xsl-launcher[aria-expanded=true] { border-color: #8ab5d455; }
|
|
24
|
+
.xsl-dir .xsl-launcher:focus-visible { outline: 2px solid #95b9d566; outline-offset: 3px; }
|
|
25
|
+
.xsl-popover { position: absolute; right: 0; bottom: 56px; width: min(390px, calc(100vw - 32px)); max-height: calc(100dvh - 110px); overflow-y: auto; overscroll-behavior: contain; border: 1px solid var(--xsl-line); border-radius: 16px; background: var(--dsw-alias-bg-base, #202020); box-shadow: 0 12px 48px #0004; }
|
|
26
|
+
.xsl-popover[hidden] { display: none; }
|
|
27
|
+
.xsl-popover .xsl-panel { height: auto; overflow: visible; }
|
|
28
|
+
@media (max-width: 480px) { .xsl-floating { right: 16px; bottom: 16px; } }
|
|
29
|
+
.xsl-brand { display: flex; align-items: center; gap: 10px; }
|
|
30
|
+
.xsl-brandmark { display: grid; place-items: center; width: 36px; height: 36px; border-radius: 11px; color: #d99b62; background: color-mix(in srgb, #d99b62 12%, transparent); flex: none; }
|
|
31
|
+
.xsl-brand h3 { font-size: 15px; font-weight: 600; letter-spacing: -.2px; }
|
|
32
|
+
.xsl-brand p { margin-top: 2px; color: var(--xsl-muted); font-size: 11px; }
|
|
33
|
+
.xsl-toolbar { display: flex; gap: 3px; margin-left: auto; align-self: flex-start; }
|
|
34
|
+
.xsl-icon-button { border: 0; border-radius: 7px; display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 6px; color: var(--xsl-muted); background: transparent; transition: background .15s, color .15s; }
|
|
35
|
+
.xsl-icon-button:hover:not(:disabled) { color: var(--xsl-text); background: var(--xsl-surface); }
|
|
36
|
+
.xsl-steps { display: flex; align-items: center; gap: 7px; padding: 20px 0 22px; color: var(--xsl-muted); font-size: 11px; }
|
|
37
|
+
.xsl-step { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
|
38
|
+
.xsl-step > span { width: 18px; height: 18px; border: 1px solid var(--xsl-line); border-radius: 50%; display: grid; place-items: center; font-size: 10px; }
|
|
39
|
+
.xsl-step[data-active=true] { color: var(--xsl-text); }
|
|
40
|
+
.xsl-step[data-active=true] > span { background: var(--xsl-surface); border-color: var(--xsl-muted); }
|
|
41
|
+
.xsl-step[data-done=true] > span { background: color-mix(in srgb, var(--xsl-accent) 12%, transparent); color: var(--xsl-accent); border-color: transparent; }
|
|
42
|
+
.xsl-step-line { height: 1px; min-width: 8px; flex: 1; background: var(--xsl-line); }
|
|
43
|
+
.xsl-fields { display: grid; gap: 16px; }
|
|
44
|
+
.xsl-field { display: grid; gap: 7px; min-width: 0; }
|
|
45
|
+
.xsl-label { display: flex; align-items: center; justify-content: space-between; gap: 6px; font-size: 12px; font-weight: 500; }
|
|
46
|
+
.xsl-count { color: var(--xsl-muted); font-weight: 400; font-size: 11px; }
|
|
47
|
+
.xsl-select { position: relative; display: flex; align-items: center; min-width: 0; }
|
|
48
|
+
.xsl-select > .xsl-field-icon { position: absolute; left: 12px; color: var(--xsl-muted); pointer-events: none; }
|
|
49
|
+
.xsl-select > .xsl-chevron { position: absolute; right: 11px; color: var(--xsl-muted); pointer-events: none; }
|
|
50
|
+
.xsl-select select, .xsl-code { appearance: none; width: 100%; min-width: 0; color: var(--xsl-text); background: var(--dsw-alias-bg-base, transparent); border: 1px solid var(--xsl-line); border-radius: 9px; padding: 10px 32px 10px 35px; height: 42px; text-overflow: ellipsis; transition: border-color .15s; }
|
|
51
|
+
.xsl-select select:hover:not(:disabled), .xsl-code:hover:not(:disabled) { border-color: var(--xsl-muted); }
|
|
52
|
+
.xsl-select select:disabled { opacity: .4; }
|
|
53
|
+
.xsl-select option { color: var(--xsl-text); background: var(--dsw-alias-bg-base, #202020); }
|
|
54
|
+
.xsl-primary { display: inline-flex; align-items: center; justify-content: center; gap: 8px; width: 100%; min-height: 42px; padding: 10px 14px; background: var(--xsl-accent); color: var(--dsw-alias-label-primary-foreground, #fff); border: 0; border-radius: 9px; font-weight: 500; transition: filter .15s, transform .15s; }
|
|
55
|
+
.xsl-primary:hover:not(:disabled) { filter: brightness(1.1); }
|
|
56
|
+
.xsl-primary:active:not(:disabled) { transform: translateY(1px); }
|
|
57
|
+
.xsl-hint { display: flex; align-items: flex-start; gap: 7px; font-size: 11px; color: var(--xsl-muted); line-height: 1.7; margin-top: 11px !important; }
|
|
58
|
+
.xsl-hint svg { margin-top: 2px; }
|
|
59
|
+
.xsl-account { min-width: 0; }
|
|
60
|
+
.xsl-panel > .xsl-account { border-top: 1px solid var(--xsl-line); margin-top: 22px; padding-top: 18px; }
|
|
61
|
+
.xsl-onboarding { margin-top: 16px; padding: 12px; border: 1px solid var(--xsl-line); border-radius: 10px; font-size: 12px; }
|
|
62
|
+
.xsl-onboarding summary { cursor: pointer; font-weight: 500; }
|
|
63
|
+
.xsl-onboarding ol { margin: 10px 0 0; padding-left: 19px; color: var(--xsl-muted); }
|
|
64
|
+
.xsl-onboarding li + li { margin-top: 8px; }
|
|
65
|
+
.xsl-onboarding q { display: block; margin-top: 4px; color: var(--xsl-text); user-select: text; }
|
|
66
|
+
.xsl-account-page { padding: 24px; max-width: 560px; }
|
|
67
|
+
.xsl-account-heading { font-size: 18px; font-weight: 600; margin-bottom: 6px !important; }
|
|
68
|
+
.xsl-description { font-size: 12px; color: var(--xsl-muted); line-height: 1.8; }
|
|
69
|
+
.xsl-account-summary { list-style: none; display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 12px; }
|
|
70
|
+
.xsl-account-summary::-webkit-details-marker { display: none; }
|
|
71
|
+
.xsl-account-summary > .xsl-chevron { margin-left: 4px; color: var(--xsl-muted); transition: transform .15s; }
|
|
72
|
+
.xsl-account-details[open] > summary > .xsl-chevron { transform: rotate(180deg); }
|
|
73
|
+
.xsl-status { display: inline-flex; align-items: center; gap: 5px; color: var(--xsl-muted); font-size: 10px; margin-left: auto; }
|
|
74
|
+
.xsl-status::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: #65b88c; }
|
|
75
|
+
.xsl-account-body { display: grid; gap: 14px; padding-top: 16px; }
|
|
76
|
+
.xsl-account-identity { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
77
|
+
.xsl-account-name { font-size: 12px; font-weight: 500; }
|
|
78
|
+
.xsl-plan { border: 1px solid var(--xsl-line); border-radius: 5px; padding: 1px 6px; font-size: 10px; color: var(--xsl-muted); }
|
|
79
|
+
.xsl-credits { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid var(--xsl-line); border-radius: 10px; overflow: hidden; }
|
|
80
|
+
.xsl-credit { min-width: 0; display: grid; gap: 3px; padding: 12px; }
|
|
81
|
+
.xsl-credit + .xsl-credit { border-left: 1px solid var(--xsl-line); }
|
|
82
|
+
.xsl-credit span { color: var(--xsl-muted); font-size: 11px; }
|
|
83
|
+
.xsl-credit strong { font-size: 21px; line-height: 1.5; font-weight: 500; font-variant-numeric: tabular-nums; letter-spacing: -.5px; }
|
|
84
|
+
.xsl-quota { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 5px; color: var(--xsl-muted); font-size: 11px; }
|
|
85
|
+
.xsl-account-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 14px; }
|
|
86
|
+
.xsl-text-button { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; background: transparent; border: 0; padding: 3px 0; color: var(--xsl-muted); }
|
|
87
|
+
.xsl-text-button:hover:not(:disabled) { color: var(--xsl-text); }
|
|
88
|
+
.xsl-text-button.xsl-disconnect:hover:not(:disabled) { color: #d65c5c; }
|
|
89
|
+
.xsl-connect-card { display: grid; gap: 14px; padding: 18px; border: 1px solid var(--xsl-line); border-radius: 12px; }
|
|
90
|
+
.xsl-connect-title { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 500; }
|
|
91
|
+
.xsl-login { display: grid; gap: 13px; padding: 15px; border: 1px solid var(--xsl-line); border-radius: 10px; margin-top: 14px; }
|
|
92
|
+
.xsl-login a { display: inline-flex; align-items: center; gap: 7px; color: var(--xsl-accent); text-decoration: none; font-size: 12px; }
|
|
93
|
+
.xsl-login a:hover { text-decoration: underline; }
|
|
94
|
+
.xsl-code { height: 38px; padding: 8px 11px; }
|
|
95
|
+
.xsl-message { display: flex; align-items: flex-start; gap: 7px; padding: 10px 12px; margin: 12px 0; border-radius: 8px; font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; background: color-mix(in srgb, #d65c5c 8%, transparent); color: #d65c5c; }
|
|
96
|
+
.xsl-message[data-kind=notice] { color: var(--xsl-muted); background: var(--xsl-surface); }
|
|
97
|
+
.xsl-loading { display: flex; align-items: center; gap: 7px; color: var(--xsl-muted); font-size: 11px; padding-top: 9px; }
|
|
98
|
+
.xsl-spin { animation: xsl-spin 1s linear infinite; }
|
|
99
|
+
@keyframes xsl-spin { to { transform: rotate(360deg); } }
|
|
100
|
+
@media (prefers-reduced-motion: reduce) { .xsl-dir *, .xsl-dir *::before, .xsl-dir *::after { animation: none !important; transition: none !important; } }
|
|
101
|
+
`;
|
|
102
|
+
const paths = {
|
|
103
|
+
folder: ['M3 7V5a2 2 0 012-2h5l2 3h7a2 2 0 012 2v11a2 2 0 01-2 2H5a2 2 0 01-2-2z'],
|
|
104
|
+
film: ['M4 3h16v18H4zM4 7h16M4 17h16M8 3v4M16 3v4M8 17v4M16 17v4'],
|
|
105
|
+
chat: ['M21 11.5a8.5 8.5 0 01-8.5 8.5H4l-1 1v-9.5A8.5 8.5 0 1121 11.5zM8 10h8M8 14h5'],
|
|
106
|
+
refresh: ['M20 7v5h-5M4 17v-5h5M6.1 7a7 7 0 0111.6-1L20 9M4 15l2.3 3a7 7 0 0011.6-1'],
|
|
107
|
+
close: ['M6 6l12 12M18 6L6 18'],
|
|
108
|
+
chevron: ['M6 9l6 6 6-6'], check: ['M5 12l4 4L19 6'],
|
|
109
|
+
info: ['M12 8h.01M12 11v5', 'M22 12a10 10 0 11-20 0 10 10 0 0120 0'],
|
|
110
|
+
user: ['M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2', 'M16 7a4 4 0 11-8 0 4 4 0 018 0'],
|
|
111
|
+
link: ['M10 13a5 5 0 007 0l3-3a5 5 0 00-7-7l-2 2M14 11a5 5 0 00-7 0l-3 3a5 5 0 007 7l2-2'],
|
|
112
|
+
arrow: ['M5 12h14M14 7l5 5-5 5'],
|
|
113
|
+
};
|
|
114
|
+
function Icon({ name, size = 16, className = '' }) {
|
|
115
|
+
return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6,
|
|
116
|
+
strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, className },
|
|
117
|
+
...(paths[name] || paths.info).map((d, key) => h('path', { d, key })));
|
|
118
|
+
}
|
|
119
|
+
const format = value => typeof value === 'number' ? value.toLocaleString() : '暂不可用';
|
|
120
|
+
const accountName = value => /^1\d{10}$/.test(value || '') ? value.slice(0, 3) + '****' + value.slice(-4) : value || '小算狸账号';
|
|
121
|
+
const message = (text, kind) => text && h('div', { className: 'xsl-message', role: kind === 'notice' ? 'status' : 'alert', 'data-kind': kind }, h(Icon, { name: 'info' }), text);
|
|
122
|
+
const primary = (label, props, icon = 'arrow') => h('button', { className: 'xsl-primary', ...props }, h(Icon, { name: icon }), label);
|
|
123
|
+
function AccountSection({ rpc, compact = false, onConnectionChange }) {
|
|
124
|
+
const [state, setState] = React.useState(null);
|
|
125
|
+
const [account, setAccount] = React.useState(null);
|
|
126
|
+
const [login, setLogin] = React.useState(null);
|
|
127
|
+
const [code, setCode] = React.useState('');
|
|
128
|
+
const [busy, setBusy] = React.useState(false);
|
|
129
|
+
const [error, setError] = React.useState('');
|
|
130
|
+
const refresh = async () => {
|
|
131
|
+
setAccount(null);
|
|
132
|
+
const status = await rpc('refresh');
|
|
133
|
+
setState(status); onConnectionChange?.(status.connected);
|
|
134
|
+
if (status.connected) setAccount(await rpc('account'));
|
|
135
|
+
};
|
|
136
|
+
React.useEffect(() => {
|
|
137
|
+
let mounted = true;
|
|
138
|
+
rpc('status').then(async status => {
|
|
139
|
+
if (!mounted) return;
|
|
140
|
+
setState(status); onConnectionChange?.(status.connected);
|
|
141
|
+
if (status.connected) { const value = await rpc('account'); if (mounted) setAccount(value); }
|
|
142
|
+
}).catch(err => { if (mounted) setError(err.message); });
|
|
143
|
+
return () => { mounted = false; };
|
|
144
|
+
}, [rpc, onConnectionChange]);
|
|
145
|
+
const run = fn => async () => {
|
|
146
|
+
setBusy(true); setError('');
|
|
147
|
+
try { await fn(); } catch (err) { setError(err.message); }
|
|
148
|
+
finally { setBusy(false); }
|
|
149
|
+
};
|
|
150
|
+
const begin = run(async () => { setLogin(await rpc('begin-login')); setCode(''); });
|
|
151
|
+
const finish = run(async () => {
|
|
152
|
+
const entered = code; setCode('');
|
|
153
|
+
await rpc('complete-login', { code: entered, state: login.state });
|
|
154
|
+
setLogin(null); await refresh();
|
|
155
|
+
});
|
|
156
|
+
const disconnect = run(async () => {
|
|
157
|
+
await rpc('disconnect'); setAccount(null); setLogin(null); setState({ connected: false }); setCode(''); onConnectionChange?.(false);
|
|
158
|
+
});
|
|
159
|
+
const loginForm = login && h('div', { className: 'xsl-login' },
|
|
160
|
+
h('a', { href: login.url, target: '_blank', rel: 'noopener noreferrer' }, h(Icon, { name: 'float', size: 14 }), '打开小算狸登录与授权页面'),
|
|
161
|
+
h('p', { className: 'xsl-description' }, '在网页确认授权后,将一次性授权码粘贴到下方,完成连接。'),
|
|
162
|
+
h('label', { className: 'xsl-field' }, h('span', { className: 'xsl-label' }, '一次性授权码'), h('input', {
|
|
163
|
+
className: 'xsl-code', type: 'password', value: code, autoComplete: 'off', maxLength: 128,
|
|
164
|
+
placeholder: '粘贴一次性授权码', onChange: event => setCode(event.target.value), disabled: busy })),
|
|
165
|
+
primary('完成连接', { disabled: busy || !code.trim(), onClick: finish }, 'link'));
|
|
166
|
+
const body = h('div', { className: 'xsl-account-body' },
|
|
167
|
+
h('div', { className: 'xsl-account-identity' }, h('span', { className: 'xsl-account-name' }, accountName(account?.username)),
|
|
168
|
+
h('span', { className: 'xsl-plan' }, account ? account.membership?.planName || '无有效会员' : '读取权益中')),
|
|
169
|
+
h('div', { className: 'xsl-credits' },
|
|
170
|
+
h('div', { className: 'xsl-credit' }, h('span', null, '通用积分'), h('strong', null, format(account?.credits?.availableCredits))),
|
|
171
|
+
h('div', { className: 'xsl-credit' }, h('span', null, '视频积分'), h('strong', null, format(account?.credits?.video?.remainingCredits)))),
|
|
172
|
+
account && h('div', { className: 'xsl-quota' },
|
|
173
|
+
h('span', null, '视频已用 ' + format(account.credits?.video?.usedCredits) + ' / ' + format(account.credits?.video?.limitCredits)),
|
|
174
|
+
account.membership && h('span', null, (account.membership.videoMaxResolution || '未包含视频权益') + ' · 并发 ' + format(account.membership.videoConcurrency))),
|
|
175
|
+
h('div', { className: 'xsl-account-actions' },
|
|
176
|
+
h('button', { className: 'xsl-text-button', disabled: busy, onClick: run(refresh) }, h(Icon, { name: 'refresh', size: 12 }), '刷新额度'),
|
|
177
|
+
h('button', { className: 'xsl-text-button', disabled: busy, onClick: begin }, '重新授权'),
|
|
178
|
+
h('button', { className: 'xsl-text-button xsl-disconnect', disabled: busy, onClick: disconnect }, '断开连接')),
|
|
179
|
+
h('p', { className: 'xsl-description' }, '生成使用小算狸站内权益与积分。'),
|
|
180
|
+
state?.expiresAt && h('p', { className: 'xsl-description' }, '授权有效期至 ' + new Date(state.expiresAt).toLocaleDateString()),
|
|
181
|
+
loginForm);
|
|
182
|
+
return h('section', { className: 'xsl-dir xsl-account' + (compact ? '' : ' xsl-account-page'), 'aria-label': '小算狸账户' },
|
|
183
|
+
!compact && h('div', { style: { marginBottom: 20 } }, h('h2', { className: 'xsl-account-heading' }, '小算狸导演台'),
|
|
184
|
+
h('p', { className: 'xsl-description' }, '连接账号,在对话中创作剧情、编辑分镜、生成图片和视频。')),
|
|
185
|
+
message(error),
|
|
186
|
+
state?.connected ? h('details', { className: 'xsl-account-details', open: compact ? undefined : true },
|
|
187
|
+
h('summary', { className: 'xsl-account-summary' }, h(Icon, { name: 'user', size: 15 }), '账户与权益',
|
|
188
|
+
h('span', { className: 'xsl-status' }, '已连接'), h(Icon, { name: 'chevron', className: 'xsl-chevron', size: 13 })), body)
|
|
189
|
+
: state && h('div', { className: 'xsl-connect-card' },
|
|
190
|
+
h('div', { className: 'xsl-connect-title' }, h(Icon, { name: 'link', size: 18 }), '连接你的创作空间'),
|
|
191
|
+
h('p', { className: 'xsl-description' }, '连接小算狸账号,开始创作剧情、编辑分镜,让故事走向银幕。'),
|
|
192
|
+
primary('登录并连接小算狸', { disabled: busy, onClick: begin }, 'link'), loginForm),
|
|
193
|
+
(busy || !state) && h('p', { className: 'xsl-loading', role: 'status' }, h(Icon, { name: 'refresh', className: 'xsl-spin', size: 12 }), busy ? '正在处理…' : '正在读取连接状态…'));
|
|
194
|
+
}
|
|
195
|
+
function ProjectPanel({ rpc, createChat, closePanel }) {
|
|
196
|
+
const [projects, setProjects] = React.useState([]);
|
|
197
|
+
const [projectsLoaded, setProjectsLoaded] = React.useState(false);
|
|
198
|
+
const [chapters, setChapters] = React.useState([]);
|
|
199
|
+
const [projectId, setProjectId] = React.useState('');
|
|
200
|
+
const [chapterId, setChapterId] = React.useState('');
|
|
201
|
+
const [connected, setConnected] = React.useState(null);
|
|
202
|
+
const [busy, setBusy] = React.useState(false);
|
|
203
|
+
const [error, setError] = React.useState('');
|
|
204
|
+
const [notice, setNotice] = React.useState('');
|
|
205
|
+
const selection = React.useRef(0);
|
|
206
|
+
const mounted = React.useRef(true);
|
|
207
|
+
React.useEffect(() => { mounted.current = true; return () => { mounted.current = false; ++selection.current; }; }, []);
|
|
208
|
+
const run = fn => async () => { setBusy(true); setError(''); try { await fn(); } catch (e) { setError(e.message); } finally { setBusy(false); } };
|
|
209
|
+
const refresh = async () => { const result = await rpc('projects'); if (mounted.current) { setProjects(result.projects || []); setProjectsLoaded(true); } };
|
|
210
|
+
const connectionChanged = React.useCallback(value => {
|
|
211
|
+
setConnected(value);
|
|
212
|
+
if (value) refresh().catch(e => { if (mounted.current) setError(e.message); });
|
|
213
|
+
else { ++selection.current; setProjects([]); setProjectsLoaded(false); setChapters([]); setProjectId(''); setChapterId(''); setError(''); setNotice(''); setBusy(false); }
|
|
214
|
+
}, [rpc]);
|
|
215
|
+
const chooseProject = async event => {
|
|
216
|
+
const id = event.target.value, epoch = ++selection.current;
|
|
217
|
+
setProjectId(id); setChapterId(''); setChapters([]); setError(''); setNotice('');
|
|
218
|
+
if (!id) return;
|
|
219
|
+
setBusy(true);
|
|
220
|
+
try { const result = await rpc('chapters', { projectId: id }); if (epoch === selection.current) setChapters(result.chapters || []); }
|
|
221
|
+
catch (e) { if (epoch === selection.current) setError(e.message); }
|
|
222
|
+
finally { if (epoch === selection.current) setBusy(false); }
|
|
223
|
+
};
|
|
224
|
+
const step = (n, title, active, done) => h('div', { className: 'xsl-step', 'data-active': active, 'data-done': done },
|
|
225
|
+
h('span', null, done ? h(Icon, { name: 'check', size: 11 }) : n), title);
|
|
226
|
+
const field = (label, count, icon, props, options, placeholder) => h('label', { className: 'xsl-field' },
|
|
227
|
+
h('span', { className: 'xsl-label' }, label, h('span', { className: 'xsl-count' }, count)),
|
|
228
|
+
h('span', { className: 'xsl-select' }, h(Icon, { name: icon, className: 'xsl-field-icon', size: 15 }),
|
|
229
|
+
h('select', { 'aria-label': label, ...props }, h('option', { value: '' }, placeholder),
|
|
230
|
+
...options.map(p => h('option', { key: p.id, value: p.id }, p.title || p.id))),
|
|
231
|
+
h(Icon, { name: 'chevron', className: 'xsl-chevron', size: 13 })));
|
|
232
|
+
return h('section', { className: 'xsl-dir xsl-panel', 'aria-label': '小算狸工程与章节' },
|
|
233
|
+
h('div', { className: 'xsl-brand' }, h('div', { className: 'xsl-brandmark' }, h('img', { src: brandLogo, width: 30, height: 33, alt: '' })),
|
|
234
|
+
h('div', null, h('h3', null, '小算狸导演台'), h('p', null, '让你的故事,走向银幕')),
|
|
235
|
+
h('div', { className: 'xsl-toolbar' },
|
|
236
|
+
h('button', { className: 'xsl-icon-button', title: '刷新工程', 'aria-label': '刷新工程', disabled: busy || !connected, onClick: run(refresh) }, h(Icon, { name: 'refresh', size: 15 })),
|
|
237
|
+
h('button', { className: 'xsl-icon-button', title: '收起工程面板', 'aria-label': '收起工程面板', onClick: closePanel }, h(Icon, { name: 'close', size: 15 })))),
|
|
238
|
+
connected !== false && h(React.Fragment, null,
|
|
239
|
+
h('div', { className: 'xsl-steps', 'aria-label': '打开工程 → 选择章节 → 新建聊天' },
|
|
240
|
+
step(1, '选工程', !projectId, !!projectId), h('span', { className: 'xsl-step-line' }),
|
|
241
|
+
step(2, '选章节', !!projectId && !chapterId, !!chapterId), h('span', { className: 'xsl-step-line' }), step(3, '开始创作', !!chapterId, false)),
|
|
242
|
+
message(error),
|
|
243
|
+
h('div', { className: 'xsl-fields' },
|
|
244
|
+
field('打开工程', projects.length ? projects.length + ' 个工程' : '', 'folder', { value: projectId, disabled: busy || !connected, onChange: chooseProject }, projects, '选择一个工程'),
|
|
245
|
+
field('选择章节', chapters.length ? chapters.length + ' 个章节' : '', 'film', { value: chapterId, disabled: busy || !projectId, onChange: e => setChapterId(e.target.value) }, chapters, busy ? '正在读取章节…' : projectId ? '选择要创作的章节' : '请先选择工程'),
|
|
246
|
+
primary('为此章节新建聊天', { disabled: busy || !projectId || !chapterId,
|
|
247
|
+
onClick: run(async () => { const result = await createChat(projectId, chapterId); setNotice(result.notice || '已打开新的章节对话。'); }) }, 'chat')),
|
|
248
|
+
h('p', { className: 'xsl-hint' }, h(Icon, { name: 'info', size: 12 }), '每个聊天专注一章。随时回到原来的对话,接着创作。'),
|
|
249
|
+
message(notice, 'notice'),
|
|
250
|
+
connected && h('details', { className: 'xsl-onboarding', open: projectsLoaded && !busy && (!projects.length || !!projectId && !chapters.length) ? true : undefined },
|
|
251
|
+
h('summary', null, '没有工程或章节?从这里开始'),
|
|
252
|
+
h('ol', null,
|
|
253
|
+
h('li', null, '在 DSH 左侧点击「新会话」,直接发送:', h('q', null, '使用小算狸导演 Skill,帮我创建一个叫《我的第一部短片》的工程。')),
|
|
254
|
+
h('li', null, '新工程会附带空白第一章。让助手打开它,再发送你的故事与创作要求。'),
|
|
255
|
+
h('li', null, '已有工程需要新章时,在新会话中说明工程名和新章节名,让助手创建并打开。'),
|
|
256
|
+
h('li', null, '也可以回到此面板,点击刷新后选择工程、章节,再新建章节聊天。创建权限不足时,按助手提示重新授权或去导演台网页创建。')))),
|
|
257
|
+
h(AccountSection, { rpc, compact: true, onConnectionChange: connectionChanged }));
|
|
258
|
+
}
|
|
259
|
+
function FloatingDirector({ rpc, createChat }) {
|
|
260
|
+
const [open, setOpen] = React.useState(false);
|
|
261
|
+
const [visited, setVisited] = React.useState(false);
|
|
262
|
+
const root = React.useRef(null), launcher = React.useRef(null), panel = React.useRef(null);
|
|
263
|
+
const panelId = React.useId();
|
|
264
|
+
const closePanel = () => { setOpen(false); launcher.current?.focus(); };
|
|
265
|
+
React.useEffect(() => {
|
|
266
|
+
if (!open) return;
|
|
267
|
+
panel.current?.focus();
|
|
268
|
+
const outside = event => { if (!root.current?.contains(event.target)) setOpen(false); };
|
|
269
|
+
const escape = event => { if (event.key === 'Escape' && !event.defaultPrevented) { event.preventDefault(); closePanel(); } };
|
|
270
|
+
document.addEventListener('pointerdown', outside);
|
|
271
|
+
document.addEventListener('keydown', escape);
|
|
272
|
+
return () => { document.removeEventListener('pointerdown', outside); document.removeEventListener('keydown', escape); };
|
|
273
|
+
}, [open]);
|
|
274
|
+
return h('div', { className: 'xsl-dir xsl-floating', ref: root },
|
|
275
|
+
visited && h('div', { className: 'xsl-popover', id: panelId, ref: panel, role: 'dialog',
|
|
276
|
+
'aria-label': '小算狸工程面板', hidden: !open, tabIndex: -1 },
|
|
277
|
+
h(ProjectPanel, { rpc, closePanel, createChat: async (...args) => { const result = await createChat(...args); setOpen(false); return result; } })),
|
|
278
|
+
h('button', { className: 'xsl-launcher', ref: launcher, 'aria-label': '小算狸工程',
|
|
279
|
+
'aria-expanded': open, 'aria-controls': visited ? panelId : undefined, 'aria-haspopup': 'dialog',
|
|
280
|
+
onClick: () => { setVisited(true); setOpen(value => !value); } }, h('img', { src: brandLogo, width: 27, height: 30, alt: '' }), '小算狸'));
|
|
281
|
+
}
|
|
282
|
+
return { inject: ['slots', 'connection', 'sidebarRight', 'sessions', 'workspaces', 'uiWorkspace'], apply(ctx) {
|
|
283
|
+
ctx.effect(() => {
|
|
284
|
+
const element = document.createElement('style'); element.textContent = styles; document.head.appendChild(element);
|
|
285
|
+
return () => element.remove();
|
|
286
|
+
});
|
|
287
|
+
const rpc = async (endpoint, payload = {}) => {
|
|
288
|
+
const result = await ctx.connection.rpc.call('/api', 'xiaosuanli/' + endpoint, payload);
|
|
289
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
290
|
+
return result.value;
|
|
291
|
+
};
|
|
292
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: '@xiaosuanli/dsh-plugin/launcher',
|
|
293
|
+
inject: () => ({ rpc,
|
|
294
|
+
createChat: async (projectId, chapterId) => {
|
|
295
|
+
const currentSession = ctx.sidebarRight.mounted.getSnapshot();
|
|
296
|
+
const workspace = ctx.workspaces.list.getSnapshot().items.find(item => item.sessionIds.includes(currentSession));
|
|
297
|
+
const cwd = currentSession && ctx.sessions.list.getSnapshot().byId[currentSession]?.cwd;
|
|
298
|
+
const conversationId = await ctx.sessions.create(workspace ? { workspaceId: workspace.workspaceId } : cwd ? { cwd } : undefined);
|
|
299
|
+
const result = await rpc('open-chapter', { conversationId, projectId, chapterId });
|
|
300
|
+
ctx.uiWorkspace.openSession(conversationId);
|
|
301
|
+
return result;
|
|
302
|
+
},
|
|
303
|
+
}) }, FloatingDirector));
|
|
304
|
+
ctx.slots.inject('settings.section' , () => ctx.slots.register({ name: 'settings.section', id: 'xiaosuanli',
|
|
305
|
+
order: 10, label: () => '小算狸', inject: () => ({ rpc }) }, AccountSection));
|
|
306
|
+
} };
|
|
307
|
+
},
|
|
308
|
+
});
|
package/cordis.patch.yml
ADDED
package/host.mjs
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import z from '@deepseek-ai/schemastery';
|
|
3
|
+
import { credentialKey } from '@deepseek-ai/dsh-credentials';
|
|
4
|
+
import { DirectorConnect, ConnectError } from './src/connect/index.mjs';
|
|
5
|
+
|
|
6
|
+
export const name = 'xiaosuanli-connect';
|
|
7
|
+
export const inject = ['connection', 'credentials', 'tools', 'skills'];
|
|
8
|
+
export const Config = z.object({
|
|
9
|
+
siteOrigin: z.string().default('https://www.xiaosuanli.com'),
|
|
10
|
+
apiOrigin: z.string().default('https://api.xiaosuanli.com'),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const output = { schema: {}, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] };
|
|
14
|
+
const empty = { type: 'object', properties: {}, additionalProperties: false };
|
|
15
|
+
|
|
16
|
+
export async function apply(ctx, config) {
|
|
17
|
+
const key = id => credentialKey(name, id);
|
|
18
|
+
const connect = new DirectorConnect({
|
|
19
|
+
client: { id: 'xiaosuanli-dsh', channel: 'dsh' },
|
|
20
|
+
apiOrigin: config.apiOrigin, siteOrigin: config.siteOrigin,
|
|
21
|
+
store: {
|
|
22
|
+
async get(id) { const record = await ctx.credentials.readRecord(key(id)); return record?.kind === 'grant' ? record.payload : undefined; },
|
|
23
|
+
async set(id, payload) { await ctx.credentials.modifyRecord(key(id), () => ({ kind: 'grant', payload })); },
|
|
24
|
+
async delete(id) { await ctx.credentials.deleteRecord(key(id)); },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
// Capture the real user-message identity at tool-call time. A later message
|
|
28
|
+
// must never lend its write authority to an older, still-running tool call.
|
|
29
|
+
const messages = new Map(), calls = new Map(), turns = new Map(), currentTurns = new Map();
|
|
30
|
+
let claims = Promise.resolve(), messageOrder = 0;
|
|
31
|
+
const chapterMessages = new Map();
|
|
32
|
+
const claim = (step, retryAfterOpen = false) => {
|
|
33
|
+
if (step.claim && !retryAfterOpen) return step.claim;
|
|
34
|
+
step.claim = claims.catch(() => {}).then(async () => {
|
|
35
|
+
if (messages.get(step.conversationId) !== step) throw new ConnectError('USER_MESSAGE_REPLACED', '已有新的用户消息,旧操作已停止');
|
|
36
|
+
try {
|
|
37
|
+
const state = await connect.conversation(step.conversationId);
|
|
38
|
+
const chapterKey = state.projectId + '/' + state.chapterId;
|
|
39
|
+
if ((chapterMessages.get(chapterKey) ?? 0) > step.order) throw new ConnectError('USER_MESSAGE_REPLACED', '本章已有更新的用户消息,请发送新消息后继续');
|
|
40
|
+
chapterMessages.set(chapterKey, step.order);
|
|
41
|
+
return await connect.beginUserTurn(step.conversationId, step.userTurnId);
|
|
42
|
+
}
|
|
43
|
+
catch (error) { if (error.code === 'CHAPTER_NOT_OPEN') return connect.beginUserTurn(step.conversationId, step.userTurnId); throw error; }
|
|
44
|
+
});
|
|
45
|
+
claims = step.claim.catch(() => {});
|
|
46
|
+
return step.claim;
|
|
47
|
+
};
|
|
48
|
+
ctx.on('session/event', (session, event) => {
|
|
49
|
+
if (event.type === 'turn/start') {
|
|
50
|
+
currentTurns.set(session.id, event.data.turn);
|
|
51
|
+
const step = messages.get(session.id);
|
|
52
|
+
if (step) turns.set(session.id + '/' + event.data.turn, step);
|
|
53
|
+
} else if (event.type === 'user/message' && event.data?.source?.kind === 'user') {
|
|
54
|
+
const step = { conversationId: session.id, userTurnId: event.data.id, order: ++messageOrder };
|
|
55
|
+
messages.set(session.id, step);
|
|
56
|
+
turns.set(session.id + '/' + currentTurns.get(session.id), step);
|
|
57
|
+
void claim(step).catch(() => {});
|
|
58
|
+
} else if (event.type === 'tool/call') {
|
|
59
|
+
const step = turns.get(session.id + '/' + event.data.turn);
|
|
60
|
+
if (step) calls.set(session.id + '/' + event.data.callId, step);
|
|
61
|
+
} else if (event.type === 'tool/result') calls.delete(session.id + '/' + event.data?.message?.toolCallId);
|
|
62
|
+
});
|
|
63
|
+
const turn = exec => {
|
|
64
|
+
const step = calls.get(exec.agent?.session.id + '/' + exec.callId);
|
|
65
|
+
if (!step) throw new ConnectError('USER_TURN_REQUIRED', '请在当前对话发送消息后操作章节');
|
|
66
|
+
if (messages.get(step.conversationId) !== step) throw new ConnectError('USER_MESSAGE_REPLACED', '已有新消息,旧消息的操作不能继续');
|
|
67
|
+
return step;
|
|
68
|
+
};
|
|
69
|
+
const text = await readFile(new URL('./skills/xiaosuanli-director/SKILL.md', import.meta.url), 'utf8');
|
|
70
|
+
const importGuide = await readFile(new URL('./skills/xiaosuanli-director/references/import-and-remake.md', import.meta.url), 'utf8');
|
|
71
|
+
const workflow = await readFile(new URL('./skills/xiaosuanli-director/references/director-workflow.md', import.meta.url), 'utf8');
|
|
72
|
+
ctx.skills.register({ name: 'xiaosuanli-director', source: 'bundled',
|
|
73
|
+
description: '操作已授权的小算狸工程:剧情、节点、素材、图片、视频与成片。',
|
|
74
|
+
content: text.replace(/^---\n[\s\S]*?\n---\n/, '') + '\n\n' + workflow + '\n\n' + importGuide });
|
|
75
|
+
ctx.tools.register({ name: 'xiaosuanli_connection', description: '读取小算狸连接状态与已授权工程;未连接时请用户打开设置中的小算狸面板。', parameters: empty, output,
|
|
76
|
+
async execute(_args, exec) {
|
|
77
|
+
try { return await connect.conversation(exec.agent?.session.id); }
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (!['CHAPTER_NOT_OPEN', 'CONVERSATION_REQUIRED'].includes(error.code)) throw error;
|
|
80
|
+
return { ...await connect.status(), chapterOpen: false, notice: '请先列出并打开当前对话要操作的章节' };
|
|
81
|
+
}
|
|
82
|
+
} });
|
|
83
|
+
ctx.tools.register({ name: 'xiaosuanli_account', description: '查看已授权的小算狸账号、会员、额度与积分。', parameters: empty, output,
|
|
84
|
+
execute: () => connect.account() });
|
|
85
|
+
for (const operation of ['project', 'chapter']) {
|
|
86
|
+
const properties = { title: { type: 'string' }, requestId: { type: 'string' } };
|
|
87
|
+
if (operation === 'chapter') Object.assign(properties, { projectId: { type: 'string' }, scriptId: { type: 'string' }, expectedVersion: { type: 'integer' } });
|
|
88
|
+
ctx.tools.register({ name: 'xiaosuanli_create_' + operation,
|
|
89
|
+
description: operation === 'project' ? '用户要求时创建工程,附带空白第一章。仅未绑定章节的对话可用,不提交生成。' : '用户要求时在指定工程和剧本创建空白章节。不改变旧对话绑定,不解析或生成。',
|
|
90
|
+
parameters: { type: 'object', properties, required: Object.keys(properties), additionalProperties: false }, output,
|
|
91
|
+
execute: (args, exec) => { const step = turn(exec); return connect.create(operation, args, { conversationId: step.conversationId }); },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
ctx.tools.register({ name: 'xiaosuanli_projects', description: '初始化:列出已授权工程供用户选择,不改变当前对话绑定。', parameters: empty, output, execute: () => connect.projects() });
|
|
95
|
+
ctx.tools.register({ name: 'xiaosuanli_chapters', description: '列出当前已连接工程的章节目录;不读取其它章节正文,不改变当前对话。', parameters: { type: 'object', properties: { projectId: { type: 'string' } }, additionalProperties: false }, output, execute: args => connect.chapters(args.projectId) });
|
|
96
|
+
ctx.tools.register({ name: 'xiaosuanli_open_chapter', description: '为当前对话打开并固定一个章节;同一对话不能换章。不同对话可打开同一章,最后一条真实用户消息拥有操作权。先等待打开成功再调用业务工具。',
|
|
97
|
+
parameters: { type: 'object', properties: { projectId: { type: 'string' }, chapterId: { type: 'string' } }, required: ['chapterId'], additionalProperties: false }, output,
|
|
98
|
+
async execute(args, exec) {
|
|
99
|
+
const step = turn(exec);
|
|
100
|
+
await connect.openChapter(step.conversationId, args.chapterId, args.projectId);
|
|
101
|
+
await claim(step, true);
|
|
102
|
+
return connect.conversation(step.conversationId);
|
|
103
|
+
} });
|
|
104
|
+
|
|
105
|
+
let disposed = false;
|
|
106
|
+
let registrations = [];
|
|
107
|
+
let serial = Promise.resolve();
|
|
108
|
+
const clearTools = () => { for (const dispose of registrations) dispose(); registrations = []; };
|
|
109
|
+
const syncTools = async () => {
|
|
110
|
+
clearTools();
|
|
111
|
+
const state = await connect.status();
|
|
112
|
+
if (!state.connected || disposed) return;
|
|
113
|
+
const capabilities = await connect.capabilities();
|
|
114
|
+
if (disposed) return;
|
|
115
|
+
for (const tool of capabilities.tools) {
|
|
116
|
+
const parameters = structuredClone(tool.inputSchema);
|
|
117
|
+
if (parameters.properties?.projectId) {
|
|
118
|
+
delete parameters.properties.projectId;
|
|
119
|
+
parameters.required = (parameters.required ?? []).filter(key => key !== 'projectId');
|
|
120
|
+
}
|
|
121
|
+
registrations.push(ctx.tools.register({ name: 'xiaosuanli_' + tool.operation, description: tool.description, parameters, output,
|
|
122
|
+
async execute(args, exec) {
|
|
123
|
+
const step = turn(exec);
|
|
124
|
+
await claim(step);
|
|
125
|
+
return connect.execute(tool.operation, args, { signal: exec.signal, conversationId: step.conversationId, userTurnId: step.userTurnId });
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const enqueue = fn => {
|
|
131
|
+
const task = serial.then(fn);
|
|
132
|
+
serial = task.catch(() => {});
|
|
133
|
+
return task;
|
|
134
|
+
};
|
|
135
|
+
const dispatch = async (endpoint, payload) => {
|
|
136
|
+
try {
|
|
137
|
+
const value = await enqueue(async () => {
|
|
138
|
+
switch (endpoint) {
|
|
139
|
+
case 'status': return connect.status();
|
|
140
|
+
case 'account': return connect.account();
|
|
141
|
+
case 'projects': return connect.projects();
|
|
142
|
+
case 'chapters': return connect.chapters(payload?.projectId);
|
|
143
|
+
case 'conversation': return connect.conversation(payload?.conversationId);
|
|
144
|
+
case 'open-chapter': return connect.openChapter(payload?.conversationId, payload?.chapterId, payload?.projectId);
|
|
145
|
+
case 'begin-login': return connect.beginLogin();
|
|
146
|
+
case 'complete-login': {
|
|
147
|
+
if (!payload || typeof payload.code !== 'string' || payload.code.length > 128 || typeof payload.state !== 'string') throw new ConnectError('INVALID_LOGIN', '请填写当前登录的一次性授权码');
|
|
148
|
+
const result = await connect.completeLogin(payload);
|
|
149
|
+
await syncTools();
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
case 'refresh': await syncTools(); return connect.status();
|
|
153
|
+
case 'disconnect': { const result = await connect.disconnect(); clearTools(); return result; }
|
|
154
|
+
default: throw new ConnectError('NOT_FOUND', '不支持的账户操作');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return { ok: true, value };
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return { ok: false, error: { code: String(error instanceof ConnectError ? error.code : 'UNAVAILABLE'),
|
|
160
|
+
message: error instanceof ConnectError ? error.message : '小算狸连接暂不可用,请稍后重试', details: {} } };
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
// Exact Connection Fetch routes are owner-only and share the Host's authenticated carrier.
|
|
164
|
+
for (const endpoint of ['status', 'account', 'projects', 'chapters', 'conversation', 'open-chapter', 'begin-login', 'complete-login', 'refresh', 'disconnect']) {
|
|
165
|
+
ctx.connection.fetch.register({ path: '/api/xiaosuanli/' + endpoint, methods: ['POST'], requestBody: 'buffered',
|
|
166
|
+
async fetch(request) {
|
|
167
|
+
let message;
|
|
168
|
+
try {
|
|
169
|
+
const raw = await request.text();
|
|
170
|
+
if (raw.length > 2048) return new Response('request too large', { status: 413 });
|
|
171
|
+
message = JSON.parse(raw);
|
|
172
|
+
} catch { return new Response('invalid request', { status: 400 }); }
|
|
173
|
+
if (message?.type !== 'client-request' || typeof message.rpcId !== 'string' || message.rpcId.length > 128 || message.method !== 'xiaosuanli/' + endpoint) {
|
|
174
|
+
return new Response('invalid request envelope', { status: 400 });
|
|
175
|
+
}
|
|
176
|
+
return Response.json({ type: 'server-response', rpcId: message.rpcId, result: await dispatch(endpoint, message.payload) });
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
ctx.on('dispose', () => { disposed = true; clearTools(); });
|
|
181
|
+
// Network unavailability should leave the login panel usable.
|
|
182
|
+
await syncTools().catch(() => {});
|
|
183
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaosuanli/dsh-plugin",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "小算狸创作插件:在 DeepSeek Harness 中连接账号、管理导演工程并制作图片与视频",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.19.0",
|
|
8
|
+
"dsh": "0.2.1-rc.2"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./host.mjs",
|
|
12
|
+
"./client": "./client.js",
|
|
13
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"dsh": {
|
|
17
|
+
"bundle": {
|
|
18
|
+
"patch": "./cordis.patch.yml"
|
|
19
|
+
},
|
|
20
|
+
"client": {
|
|
21
|
+
"platform": "web",
|
|
22
|
+
"inject": [
|
|
23
|
+
"@deepseek-ai/dsh-client-connection",
|
|
24
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
25
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
26
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
27
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
28
|
+
"@deepseek-ai/dsh-api-workspace-controller",
|
|
29
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
|
|
35
|
+
"@deepseek-ai/schemastery": "~3.18.5"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "~4.0.5"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"host.mjs",
|
|
42
|
+
"client.js",
|
|
43
|
+
"src/connect",
|
|
44
|
+
"skills",
|
|
45
|
+
"cordis.patch.yml",
|
|
46
|
+
"README.md"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "node --test test/*.test.mjs",
|
|
50
|
+
"release": "node scripts/release.mjs"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/xiaosuanli/dsh-creative.git"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/xiaosuanli/dsh-creative#readme",
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/xiaosuanli/dsh-creative/issues"
|
|
59
|
+
},
|
|
60
|
+
"author": "Xiaosuanli",
|
|
61
|
+
"license": "UNLICENSED",
|
|
62
|
+
"keywords": [
|
|
63
|
+
"dsh-plugin",
|
|
64
|
+
"deepseek-harness",
|
|
65
|
+
"xiaosuanli",
|
|
66
|
+
"video",
|
|
67
|
+
"image",
|
|
68
|
+
"director"
|
|
69
|
+
]
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xiaosuanli-director
|
|
3
|
+
description: 在 DeepSeek Harness 中连接小算狸、选择工程章节并使用导演工具制作和编辑视频。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DSH 导演台
|
|
7
|
+
|
|
8
|
+
在 DeepSeek Harness 中使用本插件。工具尚未安装时,按[安装指引](references/install.md)安装并重启对应 profile;已有 xiaosuanli_connection 时直接连接。
|
|
9
|
+
|
|
10
|
+
1. 调用 `xiaosuanli_connection`。未连接时使用插件“小算狸”账户入口,生成链接并在系统默认浏览器授权;一次性码填插件授权入口,不发送给模型。
|
|
11
|
+
2. 点击右下角“小算狸”悬浮按钮按需展开面板,或通过聊天初始化。`xiaosuanli_projects` 呈现工程,`xiaosuanli_chapters` 呈现所选工程的章节。用户选定后 `xiaosuanli_open_chapter`;面板也可为选定章节新建聊天。工程或章节列表为空时,主动转入下方首次使用流程。
|
|
12
|
+
3. 当前对话固定一个章节。不同对话允许同章,以最后真实用户消息接管;旧对话发新消息可接管,打开/查询/后台续跑不抢权。身份由宿主钩子注入,不能自行伪造。
|
|
13
|
+
4. 用户要求新建时用 `xiaosuanli_create_project` 或 `xiaosuanli_create_chapter`;已绑定聊天需要新建其它范围时另开聊天。先读回成功回执再继续。
|
|
14
|
+
5. 读取[公共导演指引](references/director-workflow.md)。公共说明中的操作名统一加 `xiaosuanli_`;精确参数以当前工具 Schema 为准。
|
|
15
|
+
6. 返回 `editorUrl`,提醒用户可在同环境导演台网页图形化修改。网页改动后先刷新再写,保留用户修改。
|
|
16
|
+
|
|
17
|
+
账号、额度与权限来自服务端;网络错误、未上线工具或缺权限如实说明,不声称已经生成或保存。内部实现和私密数据不向用户输出。
|
|
18
|
+
|
|
19
|
+
## 首次使用:没有工程或章节
|
|
20
|
+
|
|
21
|
+
1. 引导用户先在悬浮面板连接账号、完成网页授权,并将一次性码填回插件。没有工程时,选择授权页提供的账号级工程选择/创建权限;若当前网页只能选择已有工程,提供本环境导演台主页,让用户先在网页创建,再回来授权。
|
|
22
|
+
2. 在 DSH 新会话中,用户可直接说:“使用小算狸导演 Skill,帮我创建一个叫《我的第一部短片》的工程。”已有明确名称就直接执行;缺少名称时只询问名称。调用 `xiaosuanli_create_project`,成功回执中的工程包含空白第一章,向用户说明并邀请打开;用户已要求创建并开始创作时,直接打开返回的第一章。
|
|
23
|
+
3. 用户已有工程但没有章节,或想新增章节时,先列出并确认目标工程,再读取章节目录提供的剧本与版本,调用 `xiaosuanli_create_chapter`。当前对话已绑定章节时,引导在 DSH 新会话执行创建,保留原对话。
|
|
24
|
+
4. 打开章节成功后再接收故事、画风、时长等创作要求,按公共导演指引推进。也可提醒用户点击面板刷新,选择刚创建的工程和章节,再新建章节聊天。
|
|
25
|
+
|
|
26
|
+
创建空工程/空章节不提交图片或视频生成。权限不足或创建接口不可用时说明具体原因,提供工具返回的同环境导演台链接;不让用户反复刷新空列表,不把空列表当作出错或创建成功。
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 公共导演能力与制作指引
|
|
2
|
+
|
|
3
|
+
## 导演台能做什么
|
|
4
|
+
|
|
5
|
+
- **工程与章节**:创建、列出并打开工程和章节,一个聊天固定一个章节。
|
|
6
|
+
- **小说、文章转视频**:解析正文,整理人物、场景与分镜;按用户要求浓缩剧情,再制作成片。
|
|
7
|
+
- **一键复刻视频**:读取已上传视频,解析原有剧情和镜头,按用户要求调整画风、人物或语言,接续素材、视频制作与导出。只要求解析时停在解析成果,不自动生成。
|
|
8
|
+
- **剧情与分镜编辑**:新增或修改幕、镜头、对白、制作描述、时长和生成设置,绑定人物与场景素材。
|
|
9
|
+
- **图片与视频**:创建人物/场景/物品素材图,生成单镜或整章视频,选择已有结果,恢复失败部分并导出成片。
|
|
10
|
+
- **账号与额度**:查看已授权的账号、会员、积分与可用权益。
|
|
11
|
+
|
|
12
|
+
这些是产品能力;实际操作以当前渠道已提供的工具、权限与回执为准,不能把未接入的能力说成已完成。复刻视频或较长小说时读取[导入与复刻使用说明](import-and-remake.md)。
|
|
13
|
+
|
|
14
|
+
## 单章输入限制
|
|
15
|
+
|
|
16
|
+
**每章文本最多 20,000 字;单章导入复刻的源视频最长 3 分钟(180 秒),当前视频入口接受 15~180 秒。** 提交前核对完整输入,不把字数当 token 数。超过范围时提示分章或选择视频片段,由用户明确范围;不偷偷截断,也不把超长材料切成多次请求塞进同一章。用户已要求浓缩时,可完整理解后按目标时长改编,并说明使用的是改编稿。
|
|
17
|
+
|
|
18
|
+
3 分钟是导入源视频的上限,不是所有视频都必须生成 3 分钟。短文改编时长按用户要求规划;忠实复刻默认保持所选源片段的时长和关键情节。
|
|
19
|
+
|
|
20
|
+
## 对外沟通与隐私
|
|
21
|
+
|
|
22
|
+
只向用户说明功能、操作选择、成果与可行动的错误。不要透露内部模型编排、服务拓扑、数据库、部署/运维流程、内部地址、账号配置或日志;不要读取或展示访问令牌、PKCE verifier 及他人工程数据。用户内容只用于当前授权任务,不写进公共 skill、样例或安装包。工具返回的技术回执用于恢复执行,用户通常只需看到作品进度和编辑链接。
|
|
23
|
+
|
|
24
|
+
## 创建工程、章节与图形化编辑
|
|
25
|
+
|
|
26
|
+
用户要求新建时使用 `create_project(title, requestId)`;返回的工程已含空白第一章,先呈现让用户选择,不重复创建。已有工程需要新章时,先读 `chapters` 的版本和章节所属剧本,再用 `create_chapter(projectId, scriptId, title, requestId, expectedVersion)` 创建空白章。创建不等于解析或生成;取得成功回执后才 `open_chapter`。已绑定章节的聊天要创建其它工程/章,应提示新建聊天,不能原地换章。未开放创建工具或权限不足时说明缺项,给网页入口,不能假报创建成功。
|
|
27
|
+
|
|
28
|
+
创建、打开或修改完成后,提供工具返回的 `editorUrl`,提示:“可以打开导演台网页,用图形界面继续调整剧情、分镜、素材和生成设置。”通过系统默认浏览器打开用户要求查看的链接。只使用当前环境的站点:测试包打开测试网站,正式包打开正式网站。网页支持工程链接 `/apps/automated-director?project=<工程ID>`;章节在页面内选择,不编造章节深链或内部控制台地址。用户在网页修改后先刷新工程版本,再继续编辑,保留其图形化修改。
|
|
29
|
+
|
|
30
|
+
## 原文、改编稿与时长
|
|
31
|
+
|
|
32
|
+
完整阅读用户提供的原文,理解人物动机、关系变化、主要因果、信息揭示顺序、转折、高潮和结尾;保留悬念成立所需的未知信息直到既定揭示时刻,再确定观众在目标时长内能够理解与感受到的故事。用户要求浓缩时,先自主写出可拍摄的完整改编稿:保留主因果和重要关系,合并重复解释、支线和同义对白,把适合视觉表达的信息落实到行动与画面。新增拍摄设定服务原故事和用户要求,不能把猜测当作原文事实。
|
|
33
|
+
|
|
34
|
+
改编稿明确场景、人物、可见行动、对白/声音、节奏和结尾,分配时间给事件、动作完成、对白与必要留白。先核对自然表演是否放得进目标时长;压缩叙事内容,不靠全员抢话、持续旁白或简单加速播放装下原文。时长由本次用户请求决定,不固定全片秒数、镜头数或统一分段长度。
|
|
35
|
+
|
|
36
|
+
原文留在会话来源中;改编稿标明是面向目标时长的新制作版本。有压缩/改编指令时,将**完整改编稿**交给解析 API;未要求改编时交付完整选定正文。不能静默截断原文,也不能仅传一句摘要却宣称完成全部剧情设计。
|
|
37
|
+
|
|
38
|
+
## 复用导演台解析分镜
|
|
39
|
+
|
|
40
|
+
整篇文本默认通过已有 `text_import` 创建分镜、资源卡和基础连线;逐节点创建只用于明确的局部增补,不替代解析主流程。
|
|
41
|
+
|
|
42
|
+
1. 读取 `project_get` 的 `document.currentScriptId/currentChapterId`、对应章节及 `project.currentVersion`。未解析空章可提交;已解析章节先复用结果,不覆盖用户现有工作。
|
|
43
|
+
2. `text_import` 使用当前工程、目标 `chapterId`、标题、完整制作稿、用户确定的 `targetDurationSeconds/resolution`,以及稳定 `requestId/expectedVersion`。它自动准备空章,无需先手动创建幕和镜头。权限需 `director:run` 与 `project:write`;缺项说明具体授权问题。
|
|
44
|
+
3. 保存原 `kind=import/taskId`,通过 `import_get` 查询。解析成功后 `project_get(scope=full)` 读回目标章节、幕、视频节点、资源与连接。
|
|
45
|
+
4. 核对实际节点累计时长、叙事顺序、对白可演性、关系和结尾是否符合已确定的成片意图。`targetDurationSeconds` 是规划目标,不是实际命中证明。仅修正偏离的节点、时长、制作描述或资源连接,保留正确结果。新写操作使用最新版本。
|
|
46
|
+
|
|
47
|
+
当前对话只处理已打开的一章;多章节源文先确认本章范围,其它章节分别开新对话。当前没有任意节点删除/重排或已解析章节覆盖工具,缺少能力时保留结果并指出具体缺口,不暗中复制工程绕过。
|
|
48
|
+
|
|
49
|
+
## 素材与人物一致性
|
|
50
|
+
|
|
51
|
+
读取 `image_models` 的当前可用模型、价格和 `toolParameters`。模型目录的宽能力不等于当前生图入口都支持;不编造模型、参考图或分辨率参数。
|
|
52
|
+
|
|
53
|
+
围绕要制作的视频节点确定必要人物、场景和关键物品。先复用本工程已有且适用的参考图与在途任务;仅为缺失的必要资源生图。资源卡描述稳定外观,分镜描述当时的行动、表情、位置和声音。主角跨镜头使用同一资源身份和绑定参考,不每镜新建人物。
|
|
54
|
+
|
|
55
|
+
`resource_update` 的 description 保存设定,imagePrompt 保存实际生图提示词。人物/物品未自定义 imagePrompt 时优先使用产品默认设定图版式;自定义时人物为白底横向头像与正侧背无头身体设定,物品为白底独立完整物品。视觉风格贯穿卡片、镜头和生成参数。
|
|
56
|
+
|
|
57
|
+
通过 `image_generate` 提交必要图片,记录每个任务;完整成片流程需要等待图片成功并已绑定。`image_status` 返回成功但绑定冲突时,用最新版本和 `image_bind` 复用原图。用 `resource_bind` 补缺少的同章节资源连接;已有图片但未连接不能称为视频已经采用。
|
|
58
|
+
|
|
59
|
+
## 视频制作与结果选择
|
|
60
|
+
|
|
61
|
+
`videoNodes[].description` 是下一次视频生成的有效剧情输入,幕摘要和来源正文不会自动改写各镜头。必要的描述修正写入 `node_update(kind=video)`,明确当前片段的场景、参考人物、起始状态、运镜、连续动作、表情及完整对白/声音;时间从该片段 0 秒起算,并与节点时长一致。保留整片因果和前后状态。
|
|
62
|
+
|
|
63
|
+
解析后的资源卡是占位,视频入口不会代你生成参考图。先确认已连接必需素材均有实际图片且已绑定,再读回最新版本、节点时长、参考绑定与顺序。准备计划时保存输入快照,后续补绑不自动改变原计划;恢复时核对原任务实际采用的版本和素材。单镜用 `video_generate`,明确章节/幕范围可用 `generation_start`;显式使用用户要求的 resolution/aspectRatio。补齐缺失或更新新剧情时采用 `incremental:true`,保留未变化的成功片段。需要整幕重新导演时才用 `director_start`,它可能已提交素材和视频,不重复提交相同范围。
|
|
64
|
+
|
|
65
|
+
`task_get` 查询原视频任务。生成成功后核对 `selectedResultId`;尚未选用时,从 `generation.attempts[].videoResultId` 获取与 nodeId 对应的真实成功结果,用 `result_select` 选择。绑定冲突通过选择原结果恢复,不重做收费生成。
|
|
66
|
+
|
|
67
|
+
## 异步继续与成片终点
|
|
68
|
+
|
|
69
|
+
| 类型 | 查询工具 |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| import | import_get |
|
|
72
|
+
| image | image_status |
|
|
73
|
+
| video | task_get |
|
|
74
|
+
| director | director_get |
|
|
75
|
+
| export | export_get |
|
|
76
|
+
|
|
77
|
+
以工具实际进度区分“等待、生成中、成功、失败”;已创建任务不代表已经生成,向用户使用清楚的业务描述。
|
|
78
|
+
|
|
79
|
+
用户只要求提交图片/视频时,全部受理即可报告已入队。**用户要求完整成片时,入队、图片成功或视频片段完成都不是终点**:继续处理依赖、选择结果,并用 `export_start(scope=chapter)` 导出正确上下文,`export_get` 确认成片成功。
|
|
80
|
+
|
|
81
|
+
等待调用可交给宿主支持的长等待/任务恢复能力;模型不要连续空转查询相同状态。记录原任务和当前阶段,恢复时查原任务并从其结果继续。需要结束本轮等待时明确“制作仍在进行”,不能标记整体完成或让用户逐步催促才能继续。
|
|
82
|
+
|
|
83
|
+
每次新写使用最新 `expectedVersion` 和稳定 `requestId`。响应丢失时查询原 taskId;没有 taskId 时保留原 requestId、参数和 expectedVersion 重放。版本冲突先读当前状态,新修改才换键。失败只恢复失败部分;重复同一不可恢复错误、权限或余额不足时停止盲目重试,报告真实阻点。
|
|
84
|
+
|
|
85
|
+
交付前核对导出范围覆盖所有需要的当前视频、剧情时长、画幅、分辨率和声音要求。只有实际媒体证据才能称成片时长已命中;节点时长之和仅是计划时长。返回工具提供的成片地址与工程信息;没有观看实际画面时不宣称画面质量已验收。`account` 的通用积分与视频积分分别解释,缺失值不当作零。
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 小说解析与视频复刻
|
|
2
|
+
|
|
3
|
+
## 小说、文章
|
|
4
|
+
|
|
5
|
+
单章正文不超过 20,000 字时,使用 `text_import`,读回分镜与素材卡,再按用户选定的终点继续。`import_estimate` 可预估,不代表已经解析。用户已要求压缩剧情时先写完整改编稿;没有改编要求时不要自行缩写。
|
|
6
|
+
|
|
7
|
+
较长小说的产品入口是 `novel_import`,可识别章节并在需要时等待用户选章;`import_select_chapters` 只提交用户明确选定的目录。它可能包含素材图消费,不当作免费的目录读取。当前“一对话一章节”渠道不能跨章调用整本导入时,引导用户在导演台网页导入、选章后再回到对应聊天,不绕过限制。每个制作章节仍遵守单章字数边界。
|
|
8
|
+
|
|
9
|
+
## 一键复刻视频
|
|
10
|
+
|
|
11
|
+
“一键”表示用户授权完整制作后,由助手接续完成使用步骤;一次解析成功不等于已有成片。
|
|
12
|
+
|
|
13
|
+
1. 确认用户选择的源片段、复刻或改编目标,以及画风、人物、语言等明确要求。源视频须为当前用户上传到当前工程的可用素材,时长 15~180 秒。缺少素材时给导演台链接请用户上传,不能编造素材标识,也不把任意网页链接直接当成已上传视频。
|
|
14
|
+
2. 使用 `episode_inspect(projectId, assetId)` 读取真实时长和费用,再以 `episode_import` 导入到已打开的空章节。按工具实际参数传递用户选定的语言、分辨率、画风、人物及改编要求;不硬编码人物或题材。
|
|
15
|
+
3. 用 `import_get` 查询原任务。读回完整解析成果,核对关键情节、人物关系、对白、镜头顺序与源片段时长;不要把不确定观察写成事实。已有成果先复用,修复失败步骤时保留正确内容。
|
|
16
|
+
4. 用户只要求解析时返回分镜与编辑链接。用户要求成片时继续按主 skill 完成必要素材图、视频、结果选择及章节导出。视频导入本身不会自动提交全部视频,不重复创建已有任务。
|
|
17
|
+
5. 换语言、画风或人物时保留用户希望保留的故事与因果;未授权改编时忠实处理原片。返回真实成片和工程链接,没有看过实际画面就不声称画面效果已验收。
|
|
18
|
+
|
|
19
|
+
可随时提示用户打开同环境的导演台网站,图形化调整解析结果或已生成素材。对外不描述内部实现过程、模型配置或基础设施。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# DeepSeek Harness 安装
|
|
2
|
+
|
|
3
|
+
本 skill 随小算狸 DSH 插件一起安装。缺少 `xiaosuanli_connection` 时,让用户在对应的 DSH Host 环境执行:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
dsh plugin --profile web add @xiaosuanli/dsh-plugin@1.0.0
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
重启该 profile 后,在“小算狸”悬浮面板连接账号;授权码填回插件面板。已有工具时直接使用,无需重复安装。安装只启用工具与技能,生成任务按用户要求和账号权限执行。
|
|
10
|
+
|
|
11
|
+
实际 profile 若不是 web,使用用户正在运行的 profile 名称。插件要求 Node.js ≥22.19.0 和 DeepSeek Harness 0.2.1-rc.2。
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class ConnectError extends Error {
|
|
2
|
+
constructor(code, message, data) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'ConnectError';
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.data = data;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function origin(value) {
|
|
11
|
+
const url = new URL(value);
|
|
12
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/' ||
|
|
13
|
+
(url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))) {
|
|
14
|
+
throw new ConnectError('INVALID_ORIGIN', '请配置 HTTPS 服务地址;本地开发可使用 localhost HTTP');
|
|
15
|
+
}
|
|
16
|
+
return url.origin;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function request(fetcher, base, path, { token, body, method = 'GET', signal, userTurnId } = {}) {
|
|
20
|
+
const headers = { Accept: 'application/json' };
|
|
21
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
22
|
+
if (userTurnId) headers['X-Director-User-Turn'] = userTurnId;
|
|
23
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
24
|
+
let response;
|
|
25
|
+
try {
|
|
26
|
+
const timeout = AbortSignal.timeout(60000);
|
|
27
|
+
response = await fetcher(base + path, { method, headers, redirect: 'error',
|
|
28
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
29
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
30
|
+
} catch {
|
|
31
|
+
throw new ConnectError('RESULT_UNKNOWN', '连接中断。写操作可能已接受,请按原任务或原 requestId 查询,不要重复生成');
|
|
32
|
+
}
|
|
33
|
+
let envelope;
|
|
34
|
+
try { envelope = await response.json(); }
|
|
35
|
+
catch { throw new ConnectError('INVALID_RESPONSE', '服务响应不可读取,请稍后查询原请求'); }
|
|
36
|
+
if (!response.ok || envelope.code !== 200) {
|
|
37
|
+
const code = envelope.code ?? response.status;
|
|
38
|
+
const fallback = code === 401 ? '登录已失效,请重新连接小算狸' :
|
|
39
|
+
code === 403 ? '当前授权不允许此操作,请检查工程与权限' :
|
|
40
|
+
code === 409 ? '工程版本或请求发生冲突,请读取当前状态后继续' :
|
|
41
|
+
code === 402 ? '可用积分不足,请在小算狸账户中查看' : '小算狸服务暂未完成请求,请查询原任务后重试';
|
|
42
|
+
let message = code >= 400 && code < 500 && typeof envelope.message === 'string' ? envelope.message : fallback;
|
|
43
|
+
if (token) message = message.split(token).join('[redacted]');
|
|
44
|
+
message = message.replace(/https?:\/\/\S+/g, '[service address]').slice(0, 500);
|
|
45
|
+
throw new ConnectError(code, message || fallback, envelope.data);
|
|
46
|
+
}
|
|
47
|
+
return envelope.data;
|
|
48
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { ConnectError, origin, request } from './http.mjs';
|
|
3
|
+
import { operations } from './operations.mjs';
|
|
4
|
+
export { ConnectError } from './http.mjs';
|
|
5
|
+
|
|
6
|
+
// Store belongs to the host. get/set/delete handle JSON values in a private credential store.
|
|
7
|
+
export class DirectorConnect {
|
|
8
|
+
#pending;
|
|
9
|
+
#store;
|
|
10
|
+
#fetch;
|
|
11
|
+
#key;
|
|
12
|
+
#client;
|
|
13
|
+
#api;
|
|
14
|
+
#site;
|
|
15
|
+
#revision = 0;
|
|
16
|
+
#opening = new Map();
|
|
17
|
+
|
|
18
|
+
constructor({ client, apiOrigin, siteOrigin, store, fetcher = fetch }) {
|
|
19
|
+
if (!client?.id || !client?.channel || !store) throw new TypeError('Client identity and credential store are required');
|
|
20
|
+
this.#client = Object.freeze({ ...client });
|
|
21
|
+
this.#api = origin(apiOrigin);
|
|
22
|
+
this.#site = origin(siteOrigin);
|
|
23
|
+
this.#store = store;
|
|
24
|
+
this.#fetch = fetcher;
|
|
25
|
+
this.#key = 'director-' + createHash('sha256').update(JSON.stringify([this.#api, this.#site, client.id])).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beginLogin() {
|
|
29
|
+
const verifier = randomBytes(32).toString('base64url');
|
|
30
|
+
const state = randomBytes(24).toString('base64url');
|
|
31
|
+
this.#pending = { verifier, state, expiresAt: Date.now() + 300000, revision: ++this.#revision };
|
|
32
|
+
const url = new URL('/authorize/director', this.#site);
|
|
33
|
+
url.search = new URLSearchParams({ clientId: this.#client.id, codeChallenge: createHash('sha256').update(verifier).digest('base64url'), codeChallengeMethod: 'S256', state }).toString();
|
|
34
|
+
return { url: url.href, state, expiresAt: new Date(this.#pending.expiresAt).toISOString() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async beginLoginPersisted() {
|
|
38
|
+
const result = this.beginLogin();
|
|
39
|
+
await this.#store.set(this.#key + '-pending-login', this.#pending);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async completeLogin({ code, state }) {
|
|
44
|
+
const pending = this.#pending ?? await this.#store.get(this.#key + '-pending-login');
|
|
45
|
+
if (!this.#pending && pending) this.#revision = pending.revision;
|
|
46
|
+
if (!pending || pending.state !== state || pending.expiresAt <= Date.now() || typeof code !== 'string' || !code.trim()) {
|
|
47
|
+
throw new ConnectError('INVALID_LOGIN', '登录已过期或不匹配,请重新发起授权');
|
|
48
|
+
}
|
|
49
|
+
this.#pending = undefined;
|
|
50
|
+
await this.#store.delete(this.#key + '-pending-login');
|
|
51
|
+
const grant = await request(this.#fetch, this.#api, '/api/subscriber/authorizations/token', {
|
|
52
|
+
method: 'POST', body: { clientId: this.#client.id, code: code.trim(), codeVerifier: pending.verifier },
|
|
53
|
+
});
|
|
54
|
+
if (pending.revision !== this.#revision) throw new ConnectError('LOGIN_CANCELLED', '登录已取消,请重新连接');
|
|
55
|
+
this.#validateGrant(grant);
|
|
56
|
+
const key = this.#key + '-' + (grant.projectId || 'account-' + grant.authorizationId);
|
|
57
|
+
await this.#store.set(key, { ...grant, apiOrigin: this.#api, siteOrigin: this.#site });
|
|
58
|
+
await this.#store.set(this.#key, { key });
|
|
59
|
+
return this.status();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#validateGrant(grant) {
|
|
63
|
+
if (grant?.clientId !== this.#client.id || grant?.channel !== this.#client.channel ||
|
|
64
|
+
typeof grant.accessToken !== 'string' || !grant.accessToken || typeof grant.projectId !== 'string' ||
|
|
65
|
+
!(grant.projectId === '' && grant.scopes?.includes('project:select') && typeof grant.authorizationId === 'string' && /^[a-zA-Z0-9-]{1,80}$/.test(grant.authorizationId) || /^[a-zA-Z0-9-]{1,80}$/.test(grant.projectId)) || !Array.isArray(grant.scopes) ||
|
|
66
|
+
!Number.isFinite(Date.parse(grant.expiresAt)) || Date.parse(grant.expiresAt) <= Date.now()) {
|
|
67
|
+
throw new ConnectError('INVALID_GRANT', '授权身份、渠道或有效期不匹配,请重新连接');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async #grant() {
|
|
72
|
+
const selection = await this.#store.get(this.#key);
|
|
73
|
+
if (!selection || typeof selection.key !== 'string' || !selection.key.startsWith(this.#key + '-')) {
|
|
74
|
+
throw new ConnectError('LOGIN_REQUIRED', '请先完成小算狸账号授权');
|
|
75
|
+
}
|
|
76
|
+
const grant = await this.#store.get(selection.key);
|
|
77
|
+
this.#validateGrant(grant);
|
|
78
|
+
if (grant.apiOrigin !== this.#api || grant.siteOrigin !== this.#site || selection.key !== this.#key + '-' + (grant.projectId || 'account-' + grant.authorizationId)) {
|
|
79
|
+
throw new ConnectError('INVALID_GRANT', '授权服务或工程不匹配,请重新连接');
|
|
80
|
+
}
|
|
81
|
+
return grant;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async status() {
|
|
85
|
+
try {
|
|
86
|
+
const grant = await this.#grant();
|
|
87
|
+
return { connected: true, clientId: grant.clientId, channel: grant.channel,
|
|
88
|
+
projectId: grant.projectId, scopes: grant.scopes, expiresAt: grant.expiresAt };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (!(error instanceof ConnectError)) throw error;
|
|
91
|
+
return { connected: false, clientId: this.#client.id, channel: this.#client.channel, reason: error.code };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async account() {
|
|
96
|
+
const grant = await this.#grant();
|
|
97
|
+
return request(this.#fetch, this.#api, '/api/subscriber/account', { token: grant.accessToken });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async capabilities() {
|
|
101
|
+
const grant = await this.#grant();
|
|
102
|
+
const result = await request(this.#fetch, this.#api, '/api/subscriber/automated-director/capabilities', { token: grant.accessToken });
|
|
103
|
+
if (result?.clientId !== grant.clientId || result.channel !== grant.channel || result.projectId !== grant.projectId || !Array.isArray(result.tools)) {
|
|
104
|
+
throw new ConnectError('INVALID_CAPABILITIES', '服务能力与当前授权不匹配');
|
|
105
|
+
}
|
|
106
|
+
return { ...result, tools: result.tools.filter(tool => operations.has(tool.operation) && tool.inputSchema?.type === 'object') };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#conversationKey(conversationId) {
|
|
110
|
+
if (typeof conversationId !== 'string' || !conversationId || conversationId.length > 128) throw new ConnectError('CONVERSATION_REQUIRED', '请在具体对话中打开章节');
|
|
111
|
+
return this.#key + '-conversation-' + createHash('sha256').update(conversationId).digest('hex');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async #conversationGrant(conversationId) {
|
|
115
|
+
const grant = await this.#store.get(this.#conversationKey(conversationId));
|
|
116
|
+
if (!grant) throw new ConnectError('CHAPTER_NOT_OPEN', '请先在当前对话打开工程和章节');
|
|
117
|
+
this.#validateGrant(grant);
|
|
118
|
+
if (grant.conversationId !== conversationId || !grant.chapterId || grant.apiOrigin !== this.#api || grant.siteOrigin !== this.#site) throw new ConnectError('INVALID_GRANT', '对话的工程或章节绑定不匹配');
|
|
119
|
+
return grant;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
editorUrl(projectId) {
|
|
123
|
+
const url = new URL('/apps/automated-director', this.#site);
|
|
124
|
+
if (projectId) url.searchParams.set('project', projectId);
|
|
125
|
+
return url.href;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async create(operation, input, { conversationId } = {}) {
|
|
129
|
+
this.#conversationKey(conversationId);
|
|
130
|
+
const existing = await this.#store.get(this.#conversationKey(conversationId));
|
|
131
|
+
if (existing) throw new ConnectError('CHAPTER_ALREADY_BOUND', '当前对话已固定章节,请新建对话后创建');
|
|
132
|
+
if (!['project', 'chapter'].includes(operation) || !input || typeof input !== 'object' || Array.isArray(input)) throw new ConnectError('INVALID_ARGUMENTS', '创建参数无效');
|
|
133
|
+
const allowed = operation === 'project' ? ['title', 'requestId'] : ['projectId', 'scriptId', 'title', 'requestId', 'expectedVersion'];
|
|
134
|
+
if (Object.keys(input).some(key => !allowed.includes(key))) throw new ConnectError('INVALID_ARGUMENTS', '创建请求不能覆盖身份或授权');
|
|
135
|
+
const grant = await this.#grant();
|
|
136
|
+
if (operation === 'chapter' && (!/^[a-zA-Z0-9-]{1,80}$/.test(input.projectId ?? '') || grant.projectId && grant.projectId !== input.projectId)) throw new ConnectError('PROJECT_NOT_AUTHORIZED', '请选择已授权工程');
|
|
137
|
+
const suffix = operation === 'project' ? '/projects' : '/projects/' + input.projectId + '/chapters';
|
|
138
|
+
const result = await request(this.#fetch, this.#api, '/api/subscriber/automated-director' + suffix, {
|
|
139
|
+
token: grant.accessToken, method: 'POST', body: { ...input, conversationId },
|
|
140
|
+
});
|
|
141
|
+
return { ...result, editorUrl: this.editorUrl(result.project?.id || input.projectId), notice: '可在导演台网页中图形化调整;创建后请选择并打开章节' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async projects() {
|
|
145
|
+
const grant = await this.#grant();
|
|
146
|
+
return request(this.#fetch, this.#api, '/api/subscriber/automated-director/projects', { token: grant.accessToken });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async chapters(projectId) {
|
|
150
|
+
const grant = await this.#grant();
|
|
151
|
+
projectId ||= grant.projectId;
|
|
152
|
+
if (!/^[a-zA-Z0-9-]{1,80}$/.test(projectId ?? '') || grant.projectId && grant.projectId !== projectId) throw new ConnectError('PROJECT_NOT_AUTHORIZED', '请选择已授权工程');
|
|
153
|
+
return request(this.#fetch, this.#api, '/api/subscriber/automated-director/projects/' + projectId + '/chapters', { token: grant.accessToken });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async openChapter(conversationId, chapterId, projectId) {
|
|
157
|
+
const key = this.#conversationKey(conversationId);
|
|
158
|
+
const pending = (this.#opening.get(key) ?? Promise.resolve()).catch(() => {}).then(() => this.#openChapter(conversationId, chapterId, projectId));
|
|
159
|
+
this.#opening.set(key, pending);
|
|
160
|
+
try { return await pending; }
|
|
161
|
+
finally { if (this.#opening.get(key) === pending) this.#opening.delete(key); }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async #openChapter(conversationId, chapterId, projectId) {
|
|
165
|
+
const key = this.#conversationKey(conversationId);
|
|
166
|
+
const existing = await this.#store.get(key);
|
|
167
|
+
if (existing) {
|
|
168
|
+
if (existing.chapterId !== chapterId || projectId && existing.projectId !== projectId) throw new ConnectError('CHAPTER_ALREADY_BOUND', '当前对话已固定到另一章节,请新建对话');
|
|
169
|
+
return this.conversation(conversationId);
|
|
170
|
+
}
|
|
171
|
+
const parent = await this.#grant();
|
|
172
|
+
const grant = await request(this.#fetch, this.#api, '/api/subscriber/automated-director/conversations/open', {
|
|
173
|
+
token: parent.accessToken, method: 'POST', body: { conversationId, chapterId, projectId: projectId || parent.projectId },
|
|
174
|
+
});
|
|
175
|
+
this.#validateGrant(grant);
|
|
176
|
+
if (grant.projectId !== (projectId || parent.projectId) || grant.chapterId !== chapterId || grant.conversationId !== conversationId) throw new ConnectError('INVALID_GRANT', '服务端对话绑定不匹配');
|
|
177
|
+
await this.#store.set(key, { ...grant, apiOrigin: this.#api, siteOrigin: this.#site });
|
|
178
|
+
return this.conversation(conversationId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async conversation(conversationId) {
|
|
182
|
+
const grant = await this.#conversationGrant(conversationId);
|
|
183
|
+
const state = await request(this.#fetch, this.#api, '/api/subscriber/automated-director/capabilities', { token: grant.accessToken });
|
|
184
|
+
if (state.projectId !== grant.projectId || state.chapterId !== grant.chapterId || state.sessionId !== grant.authorizationId) throw new ConnectError('INVALID_CAPABILITIES', '对话状态与固定章节不匹配');
|
|
185
|
+
return { connected: true, clientId: grant.clientId, channel: grant.channel, projectId: grant.projectId,
|
|
186
|
+
chapterId: grant.chapterId, conversationId, editorUrl: this.editorUrl(grant.projectId), canWrite: state.canWrite === true, userTurnId: state.userTurnId,
|
|
187
|
+
notice: state.canWrite ? '当前对话拥有操作权' : '打开不会接管;在此对话发送新消息后接管章节,旧消息的写入会被拒绝' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async beginUserTurn(conversationId, userTurnId) {
|
|
191
|
+
if (typeof userTurnId !== 'string' || !userTurnId || userTurnId.length > 128) throw new ConnectError('USER_TURN_REQUIRED', '缺少真实用户消息标识');
|
|
192
|
+
let grant;
|
|
193
|
+
try { grant = await this.#conversationGrant(conversationId); }
|
|
194
|
+
catch (error) { if (error.code !== 'CHAPTER_NOT_OPEN') throw error; grant = await this.#grant(); }
|
|
195
|
+
return request(this.#fetch, this.#api, '/api/subscriber/automated-director/conversations/user-turn', {
|
|
196
|
+
token: grant.accessToken, method: 'POST', body: { userTurnId, conversationId },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async execute(operation, args, { signal, conversationId, userTurnId } = {}) {
|
|
201
|
+
if (!operations.has(operation) || !args || typeof args !== 'object' || Array.isArray(args)) {
|
|
202
|
+
throw new ConnectError('INVALID_OPERATION', '不支持的导演操作');
|
|
203
|
+
}
|
|
204
|
+
const grant = await this.#conversationGrant(conversationId);
|
|
205
|
+
if ((args.projectId !== undefined && args.projectId !== grant.projectId) ||
|
|
206
|
+
['userId', 'clientId', 'channel', 'conversationId', 'userTurnId', 'billing', 'token', 'url'].some(key => key in args)) {
|
|
207
|
+
throw new ConnectError('INVALID_ARGUMENTS', '只能操作当前授权工程,身份与计费由服务端确定');
|
|
208
|
+
}
|
|
209
|
+
return request(this.#fetch, this.#api, '/api/subscriber/automated-director/tools/' + operation, {
|
|
210
|
+
method: 'POST', token: grant.accessToken, body: { ...args, projectId: grant.projectId }, signal, userTurnId,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async disconnect() {
|
|
215
|
+
++this.#revision;
|
|
216
|
+
this.#pending = undefined;
|
|
217
|
+
await this.#store.delete(this.#key + '-pending-login');
|
|
218
|
+
const selection = await this.#store.get(this.#key);
|
|
219
|
+
if (!selection) return { connected: false };
|
|
220
|
+
let grant;
|
|
221
|
+
try { grant = await this.#grant(); }
|
|
222
|
+
catch (error) { if (!(error instanceof ConnectError)) throw error; }
|
|
223
|
+
if (grant) {
|
|
224
|
+
try { await request(this.#fetch, this.#api, '/api/subscriber/authorizations/current', { method: 'DELETE', token: grant.accessToken }); }
|
|
225
|
+
catch (error) { if (error.code !== 401) throw error; }
|
|
226
|
+
}
|
|
227
|
+
if (typeof selection.key === 'string' && selection.key.startsWith(this.#key + '-')) await this.#store.delete(selection.key);
|
|
228
|
+
await this.#store.delete(this.#key);
|
|
229
|
+
return { connected: false };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The server owns parameter schemas and scopes. This catalog bounds the public adapter.
|
|
2
|
+
export const operations = new Set([
|
|
3
|
+
'project_get', 'node_create', 'node_update', 'resource_create', 'resource_update',
|
|
4
|
+
'resource_import', 'resource_bind', 'image_generate', 'image_status', 'image_inspect',
|
|
5
|
+
'image_bind', 'video_generate', 'task_get', 'task_cancel', 'task_retry', 'context_select',
|
|
6
|
+
'chapter_prepare', 'video_transition', 'result_select', 'import_estimate', 'text_import',
|
|
7
|
+
'novel_import', 'episode_inspect', 'episode_import', 'import_get', 'import_cancel',
|
|
8
|
+
'import_resume', 'import_select_chapters', 'director_start', 'director_retry',
|
|
9
|
+
'director_resume', 'director_get', 'director_cancel', 'generation_start', 'export_start',
|
|
10
|
+
'export_get', 'image_models',
|
|
11
|
+
]);
|