trip-moments-skill 1.1.1 → 1.2.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/.agents/skills/trip-moments/scripts/agent_ui.py +103 -53
- package/.agents/skills/trip-moments/scripts/clawbridge.py +16 -7
- package/.agents/skills/trip-moments/scripts/journey_ui.py +7 -14
- package/.agents/skills/trip-moments/scripts/selftest.py +110 -32
- package/.agents/skills/trip-moments/scripts/studio.py +53 -4
- package/.agents/skills/trip-moments/scripts/studio_ui.py +309 -209
- package/README.md +9 -4
- package/bin/install.js +1 -1
- package/package.json +1 -1
|
@@ -28,18 +28,51 @@ AGENT_STYLES = r"""
|
|
|
28
28
|
.ap-compose textarea{width:100%;min-height:65px;max-height:160px;resize:vertical;padding:9px;border:1px solid var(--line-2);border-radius:7px;font-size:12px}
|
|
29
29
|
.ap-compose .row{margin-top:8px}.ap-compose .btn{white-space:normal}
|
|
30
30
|
.ap-next{width:100%;margin-top:8px}
|
|
31
|
-
.workflow-
|
|
32
|
-
.workflow-control h3{font-size:12px;margin-bottom:8px}
|
|
33
|
-
.workflow-mode{display:grid;grid-template-columns:1fr 1fr;gap:5px}
|
|
31
|
+
.workflow-mode{display:grid;grid-template-columns:1fr 1fr;gap:5px;min-width:0}
|
|
34
32
|
.workflow-mode button{padding:8px 5px;border:1px solid var(--line-2);border-radius:6px;background:var(--paper-hi);cursor:pointer;font-size:11px}
|
|
35
33
|
.workflow-mode button.on{background:var(--navy);border-color:var(--navy);color:#fff}
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
|
|
34
|
+
/* ---- the per-step action deck -------------------------------------------
|
|
35
|
+
写要求 → 点按钮 → 看结果 in one card. Before this the request box sat in the
|
|
36
|
+
middle card, the trigger at the top of the rail and the result at the bottom,
|
|
37
|
+
and five labels told the user to click 「上方」. */
|
|
38
|
+
.deck{border:1px solid var(--line-2);border-radius:10px;background:var(--card);margin-bottom:13px;overflow:hidden}
|
|
39
|
+
.deck-h{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line);
|
|
40
|
+
background:linear-gradient(120deg,#f7f5ee,#eef2ee)}
|
|
41
|
+
.deck-t{font-size:12px;font-weight:600}.deck-s{font-size:10px;color:var(--sub);text-align:right}
|
|
42
|
+
.deck-ctl{padding:10px 12px 2px}
|
|
43
|
+
.deck-row{display:flex;align-items:center;gap:9px;margin-bottom:8px}
|
|
44
|
+
.deck-k{font-size:10.5px;color:var(--sub);flex:none;width:52px}
|
|
45
|
+
.deck-row>.workflow-mode{flex:1}
|
|
46
|
+
.deck-row select{flex:1;min-width:0;padding:7px;border:1px solid var(--line-2);border-radius:6px;background:#fff;font-size:11px}
|
|
47
|
+
.deck-ctl #wf-mode-note{margin:0 0 10px}
|
|
48
|
+
.deck-write{padding:0 12px}
|
|
49
|
+
.deck-write .pref-grid{margin-top:2px}
|
|
50
|
+
.deck-write .cp-actions{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
|
|
51
|
+
.deck-write .cp-actions button{font-size:11px;padding:6px 10px;border:1px solid #becdbb;border-radius:6px;
|
|
52
|
+
background:#f4f8f2;color:#2c4a37;cursor:pointer;transition:all .16s var(--ease)}
|
|
53
|
+
.deck-write .cp-actions button:hover{background:#fff;transform:translateY(-1px)}
|
|
54
|
+
.deck-l{display:block;font-size:10.5px;color:var(--sub);margin:2px 0 5px}
|
|
55
|
+
.deck-write textarea{width:100%;min-height:62px;max-height:170px;resize:vertical;padding:9px;font-size:12px;
|
|
56
|
+
border:1px solid var(--line-2);border-radius:7px;background:#fff;font-family:inherit}
|
|
57
|
+
.deck-go{display:flex;gap:7px;flex-wrap:wrap;padding:10px 12px 0}
|
|
58
|
+
.deck-go .btn{white-space:normal}
|
|
59
|
+
.deck-go #wf-work{flex:1;min-width:120px}
|
|
60
|
+
.deck-go .note{margin:0}
|
|
61
|
+
#wf-connection{margin:8px 12px 0}
|
|
62
|
+
#wf-connection:empty{display:none}
|
|
63
|
+
.deck-brief{margin:9px 12px 0;border:1px solid var(--line);border-radius:7px;background:var(--paper-hi)}
|
|
64
|
+
.deck-brief>summary{font-size:10.5px;color:var(--sub);padding:8px 10px;cursor:pointer}
|
|
65
|
+
.deck-brief pre{white-space:pre-wrap;overflow-wrap:anywhere;font:10.5px/1.65 var(--mono);color:var(--ink-2);
|
|
66
|
+
max-height:300px;overflow:auto;padding:0 10px 9px;margin:0}
|
|
67
|
+
.deck-out{padding:11px 12px 12px;border-top:1px solid var(--line);margin-top:11px}
|
|
68
|
+
.deck-out-h{display:flex;align-items:center;gap:8px;font-size:10px;color:var(--sub);
|
|
69
|
+
text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px}
|
|
70
|
+
.deck-out-h .sp{flex:1}.deck-out-s{font-size:10px;color:var(--sub);text-transform:none;letter-spacing:0}
|
|
71
|
+
.deck-out .ai-out:empty{margin:0}
|
|
72
|
+
.deck-agent{border-top:1px dashed var(--line-2);padding:11px 12px 12px;background:#fbfcfa}
|
|
73
|
+
.deck-agent .ap-stream{max-height:340px;overflow:auto}
|
|
74
|
+
.deck-agent .ap-status{margin:0 0 8px}
|
|
75
|
+
.deck .proposal-card{margin:0 0 9px}
|
|
43
76
|
@media(max-width:720px){.agent-panel{top:8px;bottom:8px;right:8px;width:calc(100vw - 16px)}}
|
|
44
77
|
"""
|
|
45
78
|
|
|
@@ -73,57 +106,70 @@ AGENT_SHELL = r"""
|
|
|
73
106
|
AGENT_SCRIPT = r"""
|
|
74
107
|
const AP = { open: false, data: null, polling: false, timer: null, seq: -1, pending: false, draft: "", answers: {} };
|
|
75
108
|
function useAgentModel() { return S.data?.preferences?.ai_mode === "agent"; }
|
|
76
|
-
|
|
109
|
+
/* The deck's control rows. No trigger button here any more: the deck owns the
|
|
110
|
+
写要求 → 点按钮 → 看结果 order, so the button sits under the input box. */
|
|
111
|
+
function deckControlsHtml() {
|
|
77
112
|
const agent = useAgentModel();
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<option value="art" ${S.finishMode !== "local" ? "selected" : ""}>AI 艺术作品 · 使用所选模型来源</option>
|
|
89
|
-
<option value="local" ${S.finishMode === "local" ? "selected" : ""}>本地画板/九宫格 · 不调用模型</option>
|
|
90
|
-
</select><p class="note">在下方选择风格或版式,再点击「生成所选作品」。此步不打包。</p>` : ""}
|
|
91
|
-
${["crop","grade"].includes(S.step) ? `<label class="note" for="wf-scope">处理范围</label>
|
|
92
|
-
<select id="wf-scope" style="width:100%;padding:7px;border:1px solid var(--line-2);border-radius:6px">
|
|
93
|
-
<option value="current" ${S.aiScope !== "all" ? "selected" : ""}>当前第 ${S.sel} 格</option>
|
|
113
|
+
const sel = S.sel != null ? S.sel : 1;
|
|
114
|
+
return `<div class="deck-ctl">
|
|
115
|
+
<div class="deck-row"><span class="deck-k">谁来做</span>
|
|
116
|
+
<div class="workflow-mode" role="group" aria-label="AI 执行来源">
|
|
117
|
+
<button class="${agent ? "" : "on"}" aria-pressed="${!agent}" data-ai-mode="gateway">模型网关</button>
|
|
118
|
+
<button class="${agent ? "on" : ""}" aria-pressed="${agent}" data-ai-mode="agent">Agent 自带模型</button>
|
|
119
|
+
</div></div>
|
|
120
|
+
${["crop","grade"].includes(S.step) ? `<div class="deck-row"><span class="deck-k">处理范围</span>
|
|
121
|
+
<select id="wf-scope" aria-label="处理范围">
|
|
122
|
+
<option value="current" ${S.aiScope !== "all" ? "selected" : ""}>当前第 ${sel} 格</option>
|
|
94
123
|
<option value="all" ${S.aiScope === "all" ? "selected" : ""}>全部已选照片</option>
|
|
95
|
-
</select>` : ""}
|
|
96
|
-
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
</select></div>` : ""}
|
|
125
|
+
${S.step === "finish" ? `<div class="deck-row"><span class="deck-k">本次制作</span>
|
|
126
|
+
<select id="finish-mode" aria-label="本次制作方式">
|
|
127
|
+
<option value="art" ${S.finishMode !== "local" ? "selected" : ""}>AI 艺术作品 · 用所选模型来源</option>
|
|
128
|
+
<option value="local" ${S.finishMode === "local" ? "selected" : ""}>本地画板/九宫格 · 不调模型</option>
|
|
129
|
+
</select></div>` : ""}
|
|
130
|
+
<p class="note" id="wf-mode-note">${agent
|
|
131
|
+
? "用已绑定 Agent 的模型、工具与会话上下文;检查、处理、追问都在本卡完成。"
|
|
132
|
+
: "用配置的模型网关;AI 分析会发送照片预览,不进入 Agent 会话。"}</p>
|
|
133
|
+
</div>`;
|
|
134
|
+
}
|
|
135
|
+
/* One input box serves both 「本步要求」 and 「继续追问」, so the session buttons
|
|
136
|
+
live next to the trigger instead of under a second textarea. */
|
|
137
|
+
function deckSessionButtons() {
|
|
138
|
+
const active = !!AP.pending || !!(AP.data && AP.data.active);
|
|
139
|
+
if (active) {
|
|
140
|
+
return `<button class="btn btn-sm" id="ap-steer" onclick="controlAgent('steer')">补充到当前执行</button>
|
|
141
|
+
<button class="btn btn-sm ghost" id="ap-stop" onclick="controlAgent('stop')">停止本轮</button>`;
|
|
142
|
+
}
|
|
143
|
+
return `<button class="btn btn-sm" id="ap-send" onclick="sendAgentReply()">继续对话</button>`;
|
|
101
144
|
}
|
|
102
|
-
function
|
|
145
|
+
function agentStreamHtml() {
|
|
103
146
|
if (!useAgentModel()) return "";
|
|
104
|
-
return `<
|
|
105
|
-
<
|
|
147
|
+
return `<div class="deck-agent">
|
|
148
|
+
<div class="deck-out-h"><span>Agent 会话</span><span class="sp"></span>
|
|
149
|
+
<span class="deck-out-s">同一会话 · 回复不会自动跳步</span></div>
|
|
106
150
|
<div class="ap-stream" id="ap-stream">
|
|
107
151
|
<div class="ap-status" id="ap-run-status">正在读取绑定会话…</div>
|
|
108
152
|
<div id="ap-messages"></div><div id="ap-live" class="ap-live" style="display:none" role="status"></div>
|
|
109
153
|
<div id="ap-approval"></div>
|
|
110
154
|
<details class="ap-tools"><summary>本轮真实工具事件</summary><div id="ap-events"></div></details>
|
|
111
155
|
</div>
|
|
112
|
-
<div class="
|
|
113
|
-
<
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
156
|
+
<div class="row" style="margin-top:7px">
|
|
157
|
+
<button class="btn btn-sm ghost" onclick="syncAgentHistory()">同步会话回复</button>
|
|
158
|
+
<button class="btn btn-sm ghost" onclick="openAgentPanel()">管理 Agent 连接</button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>`;
|
|
161
|
+
}
|
|
162
|
+
function deckInput() { return $("deck-intent"); }
|
|
163
|
+
function deckInputText() {
|
|
164
|
+
const el = deckInput();
|
|
165
|
+
return el ? el.value.trim() : (S.copilotIntent || "").trim();
|
|
120
166
|
}
|
|
121
167
|
function startAgentPolling() {
|
|
122
168
|
if (!AP.timer) AP.timer = setInterval(() => {
|
|
123
169
|
if (AP.open || useAgentModel() || AP.data?.active) pollAgent();
|
|
124
170
|
}, 850);
|
|
125
171
|
}
|
|
126
|
-
function
|
|
172
|
+
function wireDeckControls() {
|
|
127
173
|
const finishMode = $("finish-mode");
|
|
128
174
|
if (finishMode) finishMode.addEventListener("change", () => {
|
|
129
175
|
S.finishMode = finishMode.value;
|
|
@@ -132,8 +178,6 @@ function wireWorkflow() {
|
|
|
132
178
|
document.querySelectorAll("[data-ai-mode]").forEach(el => el.addEventListener("click", () => setAIMode(el.dataset.aiMode)));
|
|
133
179
|
const scope = $("wf-scope");
|
|
134
180
|
if (scope) scope.addEventListener("change", () => { S.aiScope = scope.value; });
|
|
135
|
-
const input = $("ap-input");
|
|
136
|
-
if (input) input.addEventListener("input", () => { AP.draft = input.value; });
|
|
137
181
|
if (useAgentModel()) {
|
|
138
182
|
startAgentPolling();
|
|
139
183
|
AP.seq = -1; AP.approvalId = null;
|
|
@@ -187,7 +231,7 @@ async function runCurrentStep(kind) {
|
|
|
187
231
|
if (useAgentModel()) {
|
|
188
232
|
if (kind === "review") {
|
|
189
233
|
if (!await requireAgentConnection()) return;
|
|
190
|
-
return sendAgentMessage("review_step",
|
|
234
|
+
return sendAgentMessage("review_step", deckInputText(), S.step);
|
|
191
235
|
}
|
|
192
236
|
if (S.step === "retouch") return aiEdit();
|
|
193
237
|
const operation = { select:"select9", crop:"compose", grade:S.aiScope === "all" ? "grade_all" : "grade_plan", retouch:"edit", finish:"finish", caption:"caption" }[S.step];
|
|
@@ -355,25 +399,31 @@ async function sendAgentMessage(action, message, step = S.step, options = {}) {
|
|
|
355
399
|
try {
|
|
356
400
|
const data = await post("/api/agent/send", { ...options, action, message, step, slot: S.sel });
|
|
357
401
|
if (!data.ok) { toast(data.error, "err", 6500); return false; }
|
|
358
|
-
AP.draft = "";
|
|
402
|
+
AP.draft = "";
|
|
403
|
+
const el = deckInput();
|
|
404
|
+
if (el) el.value = "";
|
|
405
|
+
S.copilotIntent = "";
|
|
406
|
+
(S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = "";
|
|
407
|
+
S.deckBrief = "";
|
|
408
|
+
pollJobs(); pollAgent();
|
|
359
409
|
return true;
|
|
360
410
|
} finally { AP.pending = false; }
|
|
361
411
|
}
|
|
362
412
|
function sendAgentStep(action) {
|
|
363
|
-
return sendAgentMessage(action, (
|
|
413
|
+
return sendAgentMessage(action, deckInputText());
|
|
364
414
|
}
|
|
365
415
|
function sendAgentReply() {
|
|
366
|
-
const text = (
|
|
416
|
+
const text = deckInputText();
|
|
367
417
|
if (!text) { toast("请写下本轮回复或修改要求"); return; }
|
|
368
418
|
return sendAgentMessage("", text);
|
|
369
419
|
}
|
|
370
420
|
async function controlAgent(action, answers) {
|
|
371
|
-
const text = (
|
|
421
|
+
const text = deckInputText();
|
|
372
422
|
if (action === "steer" && !text) { toast("请先写下要补充的要求"); return; }
|
|
373
423
|
const data = await post("/api/agent/control", { action, text, answers,
|
|
374
424
|
run_id: AP.data?.run_id, request_id: AP.data?.approval?.request_id });
|
|
375
425
|
if (!data.ok) { toast(data.error, "err"); return; }
|
|
376
|
-
if (action === "steer") { AP.draft = "";
|
|
426
|
+
if (action === "steer") { AP.draft = ""; const el = deckInput(); if (el) el.value = ""; }
|
|
377
427
|
pollAgent();
|
|
378
428
|
}
|
|
379
429
|
function answerAgent() {
|
|
@@ -591,13 +591,22 @@ def _studio_instruction_impl(action: str, ctx: dict, state: dict, base: str = ""
|
|
|
591
591
|
" (也可逐格调用 POST /api/framing;ratio 为 null 表示保留原图)\n"
|
|
592
592
|
"4. 只回几行:列出你把哪几格定成了什么画幅、保留了哪几格原图。")
|
|
593
593
|
|
|
594
|
-
if action == "caption":
|
|
595
|
-
return (head + "\n请做:为这组九宫格写朋友圈文案。\n"
|
|
596
|
-
+ _slot_lines(slots) + "\n\n"
|
|
597
|
-
"写 3 版不同语气(文艺诗意 / 纪实行记 / 松弛日常),每版 1-3 句、可直接发。\n"
|
|
598
|
-
"**只写画面里真实存在的东西**;括号里是选片时的标注,必要时核图确认。"
|
|
599
|
-
"如果哪几张的标注和画面不符,在最后用一行提醒我。\n"
|
|
600
|
-
"
|
|
594
|
+
if action == "caption":
|
|
595
|
+
return (head + "\n请做:为这组九宫格写朋友圈文案。\n"
|
|
596
|
+
+ _slot_lines(slots) + "\n\n"
|
|
597
|
+
"写 3 版不同语气(文艺诗意 / 纪实行记 / 松弛日常),每版 1-3 句、可直接发。\n"
|
|
598
|
+
"**只写画面里真实存在的东西**;括号里是选片时的标注,必要时核图确认。"
|
|
599
|
+
"如果哪几张的标注和画面不符,在最后用一行提醒我。\n"
|
|
600
|
+
"写好后**必须登记到工作台**:只说在会话里不算完成,用户在工作台文案面板上"
|
|
601
|
+
"会一直看到「还没有文案」,打包时也拿不到:\n"
|
|
602
|
+
f" POST {base}/api/caption\n"
|
|
603
|
+
' {"narrative": "一句话叙事线(可留空)",\n'
|
|
604
|
+
' "versions": [{"style": "文艺诗意", "text": "…"},\n'
|
|
605
|
+
' {"style": "纪实行记", "text": "…"},\n'
|
|
606
|
+
' {"style": "松弛日常", "text": "…"}],\n'
|
|
607
|
+
' "tags": ["可选标签"]}\n'
|
|
608
|
+
"(versions 至少一版;登记后工作台会立即显示并可逐版复制)\n"
|
|
609
|
+
"最后只回一两句:三版各是什么语气。不要复述全文,用户在工作台上就能看到。")
|
|
601
610
|
|
|
602
611
|
if action == "title":
|
|
603
612
|
return (head + f"\n请做:给第 {slot_no} 格拟标题。\n"
|
|
@@ -366,33 +366,26 @@ async function renderJourneyMap() {
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
|
|
369
|
+
/* Quick direction chips for the deck. The input they used to own is now THE
|
|
370
|
+
deck input (#deck-intent) — one box per step, right above the trigger button,
|
|
371
|
+
so the old note telling the user to click the button 「上方」 is gone. */
|
|
372
|
+
function deckChipsHtml() {
|
|
370
373
|
const s = curSlot();
|
|
371
|
-
// Retouch already has a complete preset + instruction + submit workflow.
|
|
372
|
-
// A second quick-action card duplicated the input and submitted prematurely.
|
|
373
374
|
if (!s?.present || !["crop", "grade"].includes(S.step)) return "";
|
|
374
375
|
const buttons = S.step === "crop"
|
|
375
376
|
? [["compose","保护主体","保留主体完整与原有空间感,不要机械套用竖裁"],["compose","更有张力","寻找更有叙事张力的画幅,保留重要建筑与人物"]]
|
|
376
377
|
: S.step === "grade"
|
|
377
378
|
? [["grade_plan","自然通透","自然通透,克制饱和度,保持真实肤色"],["grade_plan","电影暖调","电影感暖色调,但不要压死暗部,肤色保持自然"],["grade_plan","清冷氛围","清冷克制的旅行画册影调,不要过度偏青"]]
|
|
378
379
|
: [];
|
|
379
|
-
return `<
|
|
380
|
-
|
|
381
|
-
<div class="cp-actions">${buttons.map(([action,label,intent]) =>
|
|
382
|
-
`<button data-cp-action="${action}" data-cp-intent="${H(intent)}">${label}</button>`).join("")}</div>
|
|
383
|
-
<input id="cp-intent" aria-label="本步 AI 要求" placeholder="可选:选择或填写本步要求" value="${H(S.copilotIntent || "")}" maxlength="800">
|
|
384
|
-
</section>`;
|
|
380
|
+
return `<div class="cp-actions">${buttons.map(([action,label,intent]) =>
|
|
381
|
+
`<button data-cp-action="${action}" data-cp-intent="${H(intent)}">${label}</button>`).join("")}</div>`;
|
|
385
382
|
}
|
|
386
383
|
|
|
387
384
|
function wireCopilot() {
|
|
388
|
-
const input = $("cp-intent");
|
|
389
|
-
if (input) input.addEventListener("input", () => {
|
|
390
|
-
S.copilotIntent = input.value;
|
|
391
|
-
(S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = input.value;
|
|
392
|
-
});
|
|
393
385
|
document.querySelectorAll("[data-cp-action]").forEach(el => el.addEventListener("click", () => {
|
|
394
386
|
S.copilotIntent = el.dataset.cpIntent;
|
|
395
387
|
(S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = S.copilotIntent;
|
|
388
|
+
const input = $("deck-intent");
|
|
396
389
|
if (input) input.value = S.copilotIntent;
|
|
397
390
|
document.querySelectorAll("[data-cp-action]").forEach(button =>
|
|
398
391
|
button.setAttribute("aria-pressed", String(button === el)));
|
|
@@ -2917,10 +2917,35 @@ def t_claw_grade_candidates_and_output_visibility():
|
|
|
2917
2917
|
html = studio_ui.STUDIO_HTML
|
|
2918
2918
|
assert 'id="ai-out"' not in html, \
|
|
2919
2919
|
"还有面板在用重复的 id=\"ai-out\"(getElementById 只会拿到第一个,结果会画进隐藏面板)"
|
|
2920
|
-
assert
|
|
2921
|
-
"
|
|
2922
|
-
assert '[data-pane="${step}"] .ai-out' in html,
|
|
2923
|
-
|
|
2920
|
+
assert 'id="ai-out-${H(step)}"' in html, \
|
|
2921
|
+
"操作台没有给每一步自己的输出区 id(结果可能画到别的步)"
|
|
2922
|
+
assert '[data-pane="${step}"] .ai-out' in html, "输出区没有按当前 step 解析"
|
|
2923
|
+
assert html.count("actionDeckHtml(") >= 7, \
|
|
2924
|
+
"六个 step 应各接一个操作台(含函数定义共 7 处),实际 %d 处" % html.count("actionDeckHtml(")
|
|
2925
|
+
assert html.count('pane("') >= 6, "六个 step 面板数量不对"
|
|
2926
|
+
assert "getElementById(`ai-out-${step}`)" in html, \
|
|
2927
|
+
"outBox() 没有优先按 step 精确取输出区"
|
|
2928
|
+
|
|
2929
|
+
# 操作台的存在意义:写要求 → 点按钮 → 看结果 在同一张卡里,顺序不能倒。
|
|
2930
|
+
# 旧布局把触发按钮放在 rail 顶部、输入框放在中间卡片、结果放在最底部,
|
|
2931
|
+
# 于是五处正文直接叫用户「点击上方…」——用户抱怨“上下跑”。这里把那个
|
|
2932
|
+
# 设计约束固化成断言,而不是只写进注释。
|
|
2933
|
+
deck = html[html.index("function actionDeckHtml("):]
|
|
2934
|
+
deck = deck[:deck.index("async function loadDeckBrief")]
|
|
2935
|
+
assert deck.index('id="deck-intent"') < deck.index('id="wf-work"'), \
|
|
2936
|
+
"操作台里触发按钮又跑到输入框上面了(写→发 的顺序被倒过来)"
|
|
2937
|
+
assert deck.index('id="wf-work"') < deck.index('class="ai-out"'), \
|
|
2938
|
+
"操作台里结果区不在触发按钮下面(点完还要再找结果)"
|
|
2939
|
+
for bad in ("点击上方", "上方选择", "上方唯一的", "本页顶部"):
|
|
2940
|
+
assert bad not in html, f"还在叫用户往上看:「{bad}」——操作台就是为了去掉这种跳转"
|
|
2941
|
+
# 一个输入框:旧布局有「本步要求」和「继续追问」两个框,用户得猜往哪个里写。
|
|
2942
|
+
for gone in ('id="cp-intent"', 'id="ap-input"', 'id="inst"'):
|
|
2943
|
+
assert gone not in html, f"旧输入框 {gone} 又回来了(应只剩 #deck-intent 一个)"
|
|
2944
|
+
assert "function deckInputText" in html and "deckInputText()" in html, \
|
|
2945
|
+
"发送路径没有统一读操作台的输入框"
|
|
2946
|
+
assert html.count('id="deck-intent"') == 1, \
|
|
2947
|
+
"#deck-intent 应只在操作台模板里出现一次,实际 %d 次" % html.count('id="deck-intent"')
|
|
2948
|
+
|
|
2924
2949
|
assert "p.export_zip || p.export_dir" not in html, \
|
|
2925
2950
|
"导出目录的 Windows 路径又进了 href(浏览器里点了没反应)"
|
|
2926
2951
|
assert "/api/reveal" in html, "出品台没有「在资源管理器里打开」的入口"
|
|
@@ -2930,24 +2955,29 @@ def t_claw_grade_candidates_and_output_visibility():
|
|
|
2930
2955
|
|
|
2931
2956
|
|
|
2932
2957
|
def t_agent_model_grade_candidates():
|
|
2933
|
-
"""Agent
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2958
|
+
"""Agent 自带模型轨道:每一步的结果都要落到工作台上,不能只在会话里说。
|
|
2959
|
+
|
|
2960
|
+
2026-10-04 用户实测两处空白:
|
|
2961
|
+
a) 挂着 Agent 会话点「生成调色方案」,页面既没有 克制/标准/浓郁 三行,也没有每行的
|
|
2962
|
+
预览/采用 按钮。三处协同缺口:简报把「工作台会自动生成三套候选」改写掉了、
|
|
2963
|
+
agent_turn 的 work() 不像 clawhive 轨道那样从落地的配方推导候选、前端又在
|
|
2964
|
+
useAgentModel() 下把候选面板整个隐藏。
|
|
2965
|
+
b) 点「生成 3 版文案」后,文案面板仍然回「还没有文案」——因为 Agent 根本没有能
|
|
2966
|
+
登记文案的接口(选片/裁切/调色/修图/出片都有,只有文案没有)。
|
|
2967
|
+
|
|
2968
|
+
这里用真实 HTTP 跑 /api/agent/send,会话的 run() 换成「通过工作台自己的接口落地结果」
|
|
2969
|
+
——真实 agent 就是这么做的——然后断言:三档候选真的随任务结果回到页面;Agent 只说不
|
|
2970
|
+
做时不拿旧配方冒充本轮方案;文案登记后 state 真的带得回来、空数组会被拒。
|
|
2943
2971
|
"""
|
|
2944
2972
|
import tempfile as _tf
|
|
2945
2973
|
import threading
|
|
2946
2974
|
import time as _time
|
|
2975
|
+
import urllib.error
|
|
2947
2976
|
import urllib.request
|
|
2948
2977
|
from pathlib import Path as _P
|
|
2949
2978
|
|
|
2950
2979
|
import agent_session
|
|
2980
|
+
import clawbridge
|
|
2951
2981
|
import studio
|
|
2952
2982
|
import studio_ui
|
|
2953
2983
|
|
|
@@ -2958,19 +2988,34 @@ def t_agent_model_grade_candidates():
|
|
|
2958
2988
|
old_started = getattr(studio.StudioHandler, "started", None)
|
|
2959
2989
|
box = {}
|
|
2960
2990
|
|
|
2991
|
+
def post(path, payload):
|
|
2992
|
+
req = urllib.request.Request(
|
|
2993
|
+
f"http://127.0.0.1:{box['port']}{path}",
|
|
2994
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
2995
|
+
headers={"Content-Type": "application/json"}, method="POST")
|
|
2996
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
2997
|
+
return r.status, json.loads(r.read())
|
|
2998
|
+
|
|
2961
2999
|
def fake_run(self, message, progress=None, cancelled=None):
|
|
2962
|
-
# A real agent on this track
|
|
2963
|
-
#
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3000
|
+
# A real agent on this track lands its result through the workbench's own
|
|
3001
|
+
# endpoint, so everything downstream must be derived from APPLIED state.
|
|
3002
|
+
mode = box.get("mode")
|
|
3003
|
+
text = "这一轮我没动工作台。"
|
|
3004
|
+
if mode == "grade":
|
|
3005
|
+
status, _ = post("/api/grade", {"slot": 5, "preset": "cine",
|
|
3006
|
+
"grade": {"exposure": 0.2, "highlights": -20,
|
|
3007
|
+
"shadows": 15, "dehaze": 12}})
|
|
3008
|
+
assert status == 200, f"agent 落地配方失败: HTTP {status}"
|
|
3009
|
+
text = "第 5 格我上了电影青橙。"
|
|
3010
|
+
elif mode == "caption":
|
|
3011
|
+
status, _ = post("/api/caption", {
|
|
3012
|
+
"narrative": "沿着海岸线走了一天",
|
|
3013
|
+
"versions": [{"style": "文艺诗意", "text": "潮声比人声多"},
|
|
3014
|
+
{"style": "纪实行记", "text": "今天走了两万步"},
|
|
3015
|
+
{"style": "松弛日常", "text": "风大,头发乱"}],
|
|
3016
|
+
"tags": ["海边"]})
|
|
3017
|
+
assert status == 200, f"agent 登记文案失败: HTTP {status}"
|
|
3018
|
+
text = "三版写好了,已登记到工作台。"
|
|
2974
3019
|
# Mirror how the real run() ends: the terminal event is what takes the
|
|
2975
3020
|
# session out of ACTIVE, so the next turn is allowed to start.
|
|
2976
3021
|
self.live_text = text
|
|
@@ -3007,12 +3052,12 @@ def t_agent_model_grade_candidates():
|
|
|
3007
3052
|
box["port"] = port
|
|
3008
3053
|
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
|
3009
3054
|
try:
|
|
3010
|
-
def send_turn():
|
|
3055
|
+
def send_turn(step="grade", operation="grade_plan"):
|
|
3011
3056
|
req = urllib.request.Request(
|
|
3012
3057
|
f"http://127.0.0.1:{port}/api/agent/send",
|
|
3013
|
-
data=json.dumps({"step":
|
|
3014
|
-
"operation":
|
|
3015
|
-
"message": "
|
|
3058
|
+
data=json.dumps({"step": step, "action": "work_step",
|
|
3059
|
+
"operation": operation, "slot": 5,
|
|
3060
|
+
"message": "请用你自己的模型处理这一步"}).encode("utf-8"),
|
|
3016
3061
|
headers={"Content-Type": "application/json"}, method="POST")
|
|
3017
3062
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
3018
3063
|
assert r.status == 202, f"Agent 回合没进任务队列: HTTP {r.status}"
|
|
@@ -3029,7 +3074,7 @@ def t_agent_model_grade_candidates():
|
|
|
3029
3074
|
raise AssertionError(f"agent_turn 没跑完: {jid}")
|
|
3030
3075
|
|
|
3031
3076
|
# ① agent 通过工作台接口落了配方 -> 必须带三档候选
|
|
3032
|
-
box["
|
|
3077
|
+
box["mode"] = "grade"
|
|
3033
3078
|
job = wait_job(send_turn())
|
|
3034
3079
|
assert job["status"] == "done", f"agent 回合失败: {job.get('error') or job}"
|
|
3035
3080
|
res = (job.get("result") or {}).get("result") or {}
|
|
@@ -3046,11 +3091,40 @@ def t_agent_model_grade_candidates():
|
|
|
3046
3091
|
assert (res.get("text") or ""), "agent 的话也要在同一个结果里"
|
|
3047
3092
|
|
|
3048
3093
|
# ② 一轮什么都不改的 agent -> 不得拿旧配方冒充本轮方案
|
|
3049
|
-
box["
|
|
3094
|
+
box["mode"] = "none"
|
|
3050
3095
|
job2 = wait_job(send_turn())
|
|
3051
3096
|
res2 = (job2.get("result") or {}).get("result") or {}
|
|
3052
3097
|
assert not (res2.get("candidates") or []), \
|
|
3053
3098
|
"agent 没动工作台却给了候选(旧配方被冒充成本轮方案)"
|
|
3099
|
+
|
|
3100
|
+
# ③ 文案:agent 登记后必须进 workspace(能刷新回来、算作本步已完成)
|
|
3101
|
+
box["mode"] = "caption"
|
|
3102
|
+
job3 = wait_job(send_turn(step="caption", operation="caption"))
|
|
3103
|
+
assert job3["status"] == "done", f"文案回合失败: {job3.get('error') or job3}"
|
|
3104
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/api/state", timeout=30) as r:
|
|
3105
|
+
state = json.loads(r.read())
|
|
3106
|
+
caps = (state.get("captions") or {}).get("versions") or []
|
|
3107
|
+
assert len(caps) == 3, f"agent 登记的文案没进 workspace: {state.get('captions')}"
|
|
3108
|
+
assert caps[0]["style"] == "文艺诗意" and caps[0]["text"], caps
|
|
3109
|
+
assert (state.get("captions") or {}).get("narrative") == "沿着海岸线走了一天"
|
|
3110
|
+
|
|
3111
|
+
# ④ 空文案必须被拒:不能把「没写」当成「已登记」
|
|
3112
|
+
try:
|
|
3113
|
+
post("/api/caption", {"versions": []})
|
|
3114
|
+
raise AssertionError("空 versions 不应被接受")
|
|
3115
|
+
except urllib.error.HTTPError as exc:
|
|
3116
|
+
assert exc.code == 400, f"空文案应回 400,实际 {exc.code}"
|
|
3117
|
+
try:
|
|
3118
|
+
post("/api/caption", {"versions": [" "]})
|
|
3119
|
+
raise AssertionError("全空白文案不应被接受")
|
|
3120
|
+
except urllib.error.HTTPError as exc:
|
|
3121
|
+
assert exc.code == 400, f"全空白文案应回 400,实际 {exc.code}"
|
|
3122
|
+
|
|
3123
|
+
# ⑤ 简报必须告诉 agent 登记哪个接口,否则它只会写在会话里
|
|
3124
|
+
brief = clawbridge.studio_instruction(
|
|
3125
|
+
"caption", {"slots": [1]}, state, f"http://127.0.0.1:{port}")
|
|
3126
|
+
assert "/api/caption" in brief, "文案简报没给出登记接口"
|
|
3127
|
+
assert "POST" in brief and "versions" in brief, "文案简报没有可照抄的请求体"
|
|
3054
3128
|
finally:
|
|
3055
3129
|
agent_session.AgentSession.run = real_run
|
|
3056
3130
|
httpd.shutdown()
|
|
@@ -3080,6 +3154,10 @@ def t_agent_model_grade_candidates():
|
|
|
3080
3154
|
"agent 只说不做时页面又保持沉默(用户分不清是 bug 还是没改动)"
|
|
3081
3155
|
assert "S.aiCandidates = null" in branch, \
|
|
3082
3156
|
"agent 没落地配方时没有清掉上一轮的候选(旧方案会被当成本轮产物)"
|
|
3157
|
+
assert "akind === \"caption\"" in branch, \
|
|
3158
|
+
"agent_turn 完成后没有处理文案(面板又会一直回「还没有文案」)"
|
|
3159
|
+
assert "没把文案登记到工作台" in branch, \
|
|
3160
|
+
"agent 只把文案写在会话里时页面不说明"
|
|
3083
3161
|
|
|
3084
3162
|
|
|
3085
3163
|
CASES = {
|
|
@@ -1646,7 +1646,7 @@ class Workspace:
|
|
|
1646
1646
|
"slots": slots,
|
|
1647
1647
|
"pool": pool,
|
|
1648
1648
|
"pool_meta": {"total": len(pool) + sum(1 for s in slots if s["present"])},
|
|
1649
|
-
"captions": self.captions,
|
|
1649
|
+
"captions": self.captions,
|
|
1650
1650
|
"gps": _load_json(self.outdir / "gps.json", []),
|
|
1651
1651
|
"products": products,
|
|
1652
1652
|
"selection": getattr(self, "selection_extra", {}),
|
|
@@ -2146,7 +2146,8 @@ class StudioHandler(BaseHTTPRequestHandler):
|
|
|
2146
2146
|
"/api/reset": self._api_reset,
|
|
2147
2147
|
"/api/undo": self._api_undo,
|
|
2148
2148
|
"/api/edit-source": self._api_edit_source,
|
|
2149
|
-
"/api/edit-result": self._api_edit_result,
|
|
2149
|
+
"/api/edit-result": self._api_edit_result,
|
|
2150
|
+
"/api/caption": self._api_caption,
|
|
2150
2151
|
"/api/edit-scan": self._api_edit_scan,
|
|
2151
2152
|
"/api/art": self._api_art,
|
|
2152
2153
|
"/api/art-preview": self._api_art_preview,
|
|
@@ -2172,8 +2173,8 @@ class StudioHandler(BaseHTTPRequestHandler):
|
|
|
2172
2173
|
if fn is None:
|
|
2173
2174
|
self.send_json({"ok": False, "error": f"未知接口 {path}"}, 404)
|
|
2174
2175
|
return
|
|
2175
|
-
if path in {"/api/framing", "/api/grade", "/api/swap", "/api/replace",
|
|
2176
|
-
"/api/reset", "/api/undo", "/api/edit-source"}:
|
|
2176
|
+
if path in {"/api/framing", "/api/grade", "/api/swap", "/api/replace",
|
|
2177
|
+
"/api/reset", "/api/undo", "/api/edit-source", "/api/caption"}:
|
|
2177
2178
|
# Snapshot + mutation + render must be atomic even when an
|
|
2178
2179
|
# agent and a browser edit the same workspace concurrently.
|
|
2179
2180
|
with self.ws._lock:
|
|
@@ -2227,6 +2228,13 @@ class StudioHandler(BaseHTTPRequestHandler):
|
|
|
2227
2228
|
self.send_json(link.sync_history())
|
|
2228
2229
|
elif path == "/api/agent/disconnect":
|
|
2229
2230
|
self.send_json(link.disconnect())
|
|
2231
|
+
elif path == "/api/agent/brief":
|
|
2232
|
+
# Lazy: the brief can be several KB (the art one embeds a compiled
|
|
2233
|
+
# prompt card), so it is fetched when the deck's details is opened
|
|
2234
|
+
# rather than riding along on every /api/state poll.
|
|
2235
|
+
self.send_json({"ok": True,
|
|
2236
|
+
"brief": str(self.ws.extra.get("agent_brief") or ""),
|
|
2237
|
+
"at": str(self.ws.extra.get("agent_brief_at") or "")})
|
|
2230
2238
|
elif path == "/api/agent/control":
|
|
2231
2239
|
self.send_json(link.control(str(body.get("action") or ""), body))
|
|
2232
2240
|
elif path == "/api/agent/send":
|
|
@@ -2316,6 +2324,12 @@ class StudioHandler(BaseHTTPRequestHandler):
|
|
|
2316
2324
|
f"修图状态:{'已采用文件缺失' if s.get('edit_missing') else '新修图待采用' if s['pending_edit'] else '已采用修图' if s['edit_adopted'] else '原片处理版'}"
|
|
2317
2325
|
for s in snapshot["slots"] if s["present"])
|
|
2318
2326
|
outbound = context + "\n当前作品:\n" + summary + "\n\n用户本轮输入:\n" + message
|
|
2327
|
+
# Keep the brief the deck shows the user. Transparency is the point:
|
|
2328
|
+
# 「它有没有照做」 becomes checkable without reading the conversation.
|
|
2329
|
+
with self.ws._lock:
|
|
2330
|
+
self.ws.extra["agent_brief"] = context
|
|
2331
|
+
self.ws.extra["agent_brief_at"] = _now()
|
|
2332
|
+
self.ws.save()
|
|
2319
2333
|
link.reserve(message)
|
|
2320
2334
|
def work(job):
|
|
2321
2335
|
result = link.run(outbound, lambda text, pct: (setattr(job, "detail", text), setattr(job, "progress", pct)),
|
|
@@ -2948,6 +2962,41 @@ class StudioHandler(BaseHTTPRequestHandler):
|
|
|
2948
2962
|
"summary": self.ws.grade_summary(slot),
|
|
2949
2963
|
"state": self.ws.state()["slots"][n - 1]})
|
|
2950
2964
|
|
|
2965
|
+
def _api_caption(self, body: dict) -> None:
|
|
2966
|
+
"""Persist caption versions produced outside the workbench (both agent tracks).
|
|
2967
|
+
|
|
2968
|
+
The local gateway track has always written captions into the workspace
|
|
2969
|
+
(see _api_ai), but an agent had no equivalent endpoint: it could only say
|
|
2970
|
+
the three versions in the conversation, so the 文案 step kept showing
|
|
2971
|
+
「还没有文案」 — on the very screen where the user had just asked for them —
|
|
2972
|
+
and the set never counted as finished. Registration stores exactly what
|
|
2973
|
+
the caller sends; nothing is invented here.
|
|
2974
|
+
"""
|
|
2975
|
+
raw = body.get("versions")
|
|
2976
|
+
if not isinstance(raw, list):
|
|
2977
|
+
self.send_json({"ok": False, "error": "versions 必须是数组"}, 400)
|
|
2978
|
+
return
|
|
2979
|
+
versions = []
|
|
2980
|
+
for item in raw:
|
|
2981
|
+
if isinstance(item, str):
|
|
2982
|
+
text, style = item.strip(), ""
|
|
2983
|
+
elif isinstance(item, dict):
|
|
2984
|
+
text = str(item.get("text") or "").strip()
|
|
2985
|
+
style = str(item.get("style") or "").strip()
|
|
2986
|
+
else:
|
|
2987
|
+
continue
|
|
2988
|
+
if text:
|
|
2989
|
+
versions.append({"style": style or f"版本 {len(versions) + 1}",
|
|
2990
|
+
"text": text[:4000]})
|
|
2991
|
+
if not versions:
|
|
2992
|
+
self.send_json({"ok": False, "error": "至少需要一版非空文案"}, 400)
|
|
2993
|
+
return
|
|
2994
|
+
tags = [str(t).strip() for t in (body.get("tags") or []) if str(t).strip()][:12]
|
|
2995
|
+
saved = self.ws.save_captions({"narrative": str(body.get("narrative") or "")[:600],
|
|
2996
|
+
"versions": versions, "tags": tags})
|
|
2997
|
+
self.send_json({"ok": True, "captions": saved, "versions": saved["versions"],
|
|
2998
|
+
"revision": self.ws.revision})
|
|
2999
|
+
|
|
2951
3000
|
def _api_edit_result(self, body: dict) -> None:
|
|
2952
3001
|
"""Register a retouch result produced outside the workbench.
|
|
2953
3002
|
|