trip-moments-skill 1.1.2 → 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/journey_ui.py +7 -14
- package/.agents/skills/trip-moments/scripts/selftest.py +29 -4
- package/.agents/skills/trip-moments/scripts/studio.py +14 -1
- package/.agents/skills/trip-moments/scripts/studio_ui.py +289 -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() {
|
|
@@ -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, "出品台没有「在资源管理器里打开」的入口"
|
|
@@ -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", {}),
|
|
@@ -2228,6 +2228,13 @@ class StudioHandler(BaseHTTPRequestHandler):
|
|
|
2228
2228
|
self.send_json(link.sync_history())
|
|
2229
2229
|
elif path == "/api/agent/disconnect":
|
|
2230
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 "")})
|
|
2231
2238
|
elif path == "/api/agent/control":
|
|
2232
2239
|
self.send_json(link.control(str(body.get("action") or ""), body))
|
|
2233
2240
|
elif path == "/api/agent/send":
|
|
@@ -2317,6 +2324,12 @@ class StudioHandler(BaseHTTPRequestHandler):
|
|
|
2317
2324
|
f"修图状态:{'已采用文件缺失' if s.get('edit_missing') else '新修图待采用' if s['pending_edit'] else '已采用修图' if s['edit_adopted'] else '原片处理版'}"
|
|
2318
2325
|
for s in snapshot["slots"] if s["present"])
|
|
2319
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()
|
|
2320
2333
|
link.reserve(message)
|
|
2321
2334
|
def work(job):
|
|
2322
2335
|
result = link.run(outbound, lambda text, pct: (setattr(job, "detail", text), setattr(job, "progress", pct)),
|
|
@@ -706,7 +706,7 @@ kbd{font-family:var(--mono);font-size:9.5px;border:1px solid var(--line-2);borde
|
|
|
706
706
|
<button class="btn btn-ai" onclick="saveCustomModel()">注册并应用</button>
|
|
707
707
|
<div class="field" style="margin-top:16px"><label for="cfg-amap">高德 Web 服务 Key(路线卡,可选)</label>
|
|
708
708
|
<input id="cfg-amap" type="password" autocomplete="off" placeholder="留空保留已有配置">
|
|
709
|
-
<p class="note">仅高德路线卡需要;旅程页的在线地图不使用此 Key
|
|
709
|
+
<p class="note">仅高德路线卡需要;旅程页的在线地图不使用此 Key。填好后记得保存设置。</p></div>
|
|
710
710
|
<div class="field" style="margin-top:24px;border-top:1px solid var(--line);padding-top:15px">
|
|
711
711
|
<label style="display:flex;align-items:center;gap:8px;letter-spacing:0">
|
|
712
712
|
<input id="cfg-show-claw" type="checkbox" style="width:auto" onchange="setClawVisibility(this.checked)">
|
|
@@ -1071,7 +1071,7 @@ function publishHeroHtml() {
|
|
|
1071
1071
|
${p.export_zip ? `<button class="btn btn-primary" onclick="saveZip()" ${stale ? "disabled" : ""}>下载全部 · zip</button>` : ""}
|
|
1072
1072
|
<button class="btn btn-sm ghost" onclick="revealExport()">在资源管理器里打开</button>
|
|
1073
1073
|
</div>
|
|
1074
|
-
${stale ? `<p class="note warn" style="margin-top:8px"
|
|
1074
|
+
${stale ? `<p class="note warn" style="margin-top:8px">这是上次导出,尚未确认与当前照片及参数一致。请先重新点「打包发布」,不会把旧包当作最新成品下载。</p>` : ""}
|
|
1075
1075
|
<div class="note" style="margin-top:8px;font-family:var(--mono);font-size:9.5px"
|
|
1076
1076
|
title="${H(p.export_dir || "")}">${H(shortPath(p.export_dir))}</div>
|
|
1077
1077
|
</div>`;
|
|
@@ -1553,6 +1553,105 @@ $("canvas").addEventListener("click", (e) => {
|
|
|
1553
1553
|
});
|
|
1554
1554
|
|
|
1555
1555
|
/* ---------------- inspector ---------------- */
|
|
1556
|
+
/* ---------------- the per-step action deck ----------------
|
|
1557
|
+
写要求 → 点按钮 → 看结果 used to be three separate places: the request box sat in
|
|
1558
|
+
the middle card, the trigger button at the top of the rail, and the result at the
|
|
1559
|
+
very bottom — five labels in the UI literally told the user to click the button
|
|
1560
|
+
「上方」 because the layout
|
|
1561
|
+
forced a scroll up to act and back down to read. Everything a step needs to run
|
|
1562
|
+
now sits in one card in that order, and the manual controls (presets, sliders,
|
|
1563
|
+
ratios, art styles) are demoted below it. */
|
|
1564
|
+
const DECK_LABEL = { select: "智能选片", crop: "生成构图方案", grade: "生成调色方案",
|
|
1565
|
+
retouch: "开始修图", caption: "生成 3 版文案", finish: "生成所选作品" };
|
|
1566
|
+
const DECK_INTENT = {
|
|
1567
|
+
select: ["策展偏好补充", "可选:例如 主角不超过两张,多留一张夜景"],
|
|
1568
|
+
crop: ["构图要求", "可选:例如 保留主体完整与原有空间感,不要机械套用竖裁"],
|
|
1569
|
+
grade: ["调色要求", "可选:例如 电影感暖色调,但不要压死暗部,肤色保持自然"],
|
|
1570
|
+
retouch: ["修图指令", "例如:去掉画面右侧的游客"],
|
|
1571
|
+
caption: ["文案要求", "可选:例如 三版都别用感叹号,第二版交代地点"],
|
|
1572
|
+
finish: ["出片要求", "可选:例如 标题短一点,不要盖住人脸"],
|
|
1573
|
+
};
|
|
1574
|
+
function deckIntentLabel() { return (DECK_INTENT[S.step] || ["本步要求"])[0]; }
|
|
1575
|
+
function deckIntentPlaceholder() { return (DECK_INTENT[S.step] || ["本步要求", ""]).slice(-1)[0]; }
|
|
1576
|
+
function deckIntentValue() {
|
|
1577
|
+
// ONE box serves both 「本步要求」 and the Agent follow-up: it is the same
|
|
1578
|
+
// message to the same session, and keeping two boxes is what made the flow
|
|
1579
|
+
// feel like guessing which one to type in.
|
|
1580
|
+
return S.step === "retouch" ? (S.editInstruction || S.copilotIntent || "")
|
|
1581
|
+
: (S.copilotIntent || "");
|
|
1582
|
+
}
|
|
1583
|
+
function deckBriefHtml(promptBox) {
|
|
1584
|
+
if (!useAgentModel()) return promptBox || "";
|
|
1585
|
+
return `<details class="deck-brief" id="deck-brief" ${S.briefOpen ? "open" : ""}>
|
|
1586
|
+
<summary>本轮发给 Agent 的简报</summary>
|
|
1587
|
+
<pre id="deck-brief-body">${H(S.deckBrief || "还没有发过任务;发起后这里显示它实际收到的要求。")}</pre>
|
|
1588
|
+
</details>`;
|
|
1589
|
+
}
|
|
1590
|
+
function actionDeckHtml(o = {}) {
|
|
1591
|
+
const step = S.step;
|
|
1592
|
+
const agent = useAgentModel();
|
|
1593
|
+
const busy = agent ? !!(AP.pending || (AP.data && AP.data.active))
|
|
1594
|
+
: !!(S.aiBusy || S.aiSubmissions);
|
|
1595
|
+
return `<section class="deck" id="action-deck" data-deck-step="${H(step)}">
|
|
1596
|
+
<header class="deck-h"><span class="deck-t">本步操作台</span><span class="sp"></span>
|
|
1597
|
+
<span class="deck-s">${agent ? "写要求 → 点按钮 → 结果与会话都在下面"
|
|
1598
|
+
: "写要求 → 点按钮 → 结果就在下面"}</span></header>
|
|
1599
|
+
${deckControlsHtml()}
|
|
1600
|
+
<div class="deck-write">
|
|
1601
|
+
${o.chips || ""}
|
|
1602
|
+
<label class="deck-l" for="deck-intent">${H(deckIntentLabel())}</label>
|
|
1603
|
+
<textarea id="deck-intent" spellcheck="false" maxlength="800"
|
|
1604
|
+
placeholder="${H(deckIntentPlaceholder())}">${H(deckIntentValue())}</textarea>
|
|
1605
|
+
</div>
|
|
1606
|
+
<div class="deck-go">
|
|
1607
|
+
<button class="btn btn-sm" id="wf-review" onclick="runCurrentStep('review')">检查本步</button>
|
|
1608
|
+
<button class="btn btn-ai" id="wf-work" onclick="runCurrentStep('work')"${busy ? " disabled" : ""}>${H(DECK_LABEL[step] || "处理本步")}</button>
|
|
1609
|
+
${agent ? deckSessionButtons() : ""}
|
|
1610
|
+
</div>
|
|
1611
|
+
<p class="note" id="wf-connection"${agent ? "" : ' style="display:none"'}></p>
|
|
1612
|
+
${deckBriefHtml(o.prompt)}
|
|
1613
|
+
<div class="deck-out">
|
|
1614
|
+
<div class="deck-out-h"><span>结果</span><span class="sp"></span></div>
|
|
1615
|
+
${o.result || ""}
|
|
1616
|
+
<div class="ai-out" id="ai-out-${H(step)}"></div>
|
|
1617
|
+
</div>
|
|
1618
|
+
${agent ? agentStreamHtml() : ""}
|
|
1619
|
+
</section>`;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
async function loadDeckBrief() {
|
|
1623
|
+
const body = $("deck-brief-body");
|
|
1624
|
+
if (!body) return;
|
|
1625
|
+
const d = await post("/api/agent/brief", {});
|
|
1626
|
+
if (!d.ok) { body.textContent = d.error || "读不到简报"; return; }
|
|
1627
|
+
S.deckBrief = d.brief || "";
|
|
1628
|
+
body.textContent = S.deckBrief
|
|
1629
|
+
? (d.at ? "发信时间 " + d.at + "\n\n" : "") + S.deckBrief
|
|
1630
|
+
: "还没有发过任务;发起后这里显示它实际收到的要求。";
|
|
1631
|
+
}
|
|
1632
|
+
function wireDeck() {
|
|
1633
|
+
const input = $("deck-intent");
|
|
1634
|
+
if (input) input.addEventListener("input", () => {
|
|
1635
|
+
// One store for all steps: S.copilotIntent is what aiRun(), delegateAIAction()
|
|
1636
|
+
// and promptCtx() already read, so the box the user types in is the box the
|
|
1637
|
+
// request is built from — no second copy to drift out of sync.
|
|
1638
|
+
S.copilotIntent = input.value;
|
|
1639
|
+
(S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = input.value;
|
|
1640
|
+
if (S.step === "retouch") { S.editInstruction = input.value; rememberEditDraft(); }
|
|
1641
|
+
AP.draft = input.value;
|
|
1642
|
+
});
|
|
1643
|
+
const brief = $("deck-brief");
|
|
1644
|
+
if (brief) {
|
|
1645
|
+
// Always re-read on open: the last brief changes every turn, and a stale one
|
|
1646
|
+
// would make 「它有没有照做」 unanswerable.
|
|
1647
|
+
brief.addEventListener("toggle", () => {
|
|
1648
|
+
S.briefOpen = brief.open;
|
|
1649
|
+
if (brief.open) loadDeckBrief();
|
|
1650
|
+
});
|
|
1651
|
+
if (brief.open) loadDeckBrief();
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1556
1655
|
function renderInspector(passive = true) {
|
|
1557
1656
|
if (!S.data) return;
|
|
1558
1657
|
if (passive && (isEditing() || S.mutations)) { S.inspectorDeferred = true; return; }
|
|
@@ -1561,14 +1660,18 @@ function renderInspector(passive = true) {
|
|
|
1561
1660
|
const scroll = box.scrollTop;
|
|
1562
1661
|
const s = curSlot();
|
|
1563
1662
|
if (!s) { box.innerHTML = `<div class="note" style="padding:40px 6px">没有可检查的格子。</div>`; return; }
|
|
1564
|
-
if (!s.present && !["select", "finish", "caption"].includes(S.step)) {
|
|
1565
|
-
box.innerHTML =
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1663
|
+
if (!s.present && !["select", "finish", "caption"].includes(S.step)) {
|
|
1664
|
+
box.innerHTML = `<div data-pane="${S.step}" class="on">
|
|
1665
|
+
${actionDeckHtml()}
|
|
1666
|
+
<div class="card"><div class="card-h"><span class="t">第 ${s.slot} 格是空的</span></div>
|
|
1667
|
+
<div class="note">从照片库点一张照片即可填入这一格。</div>
|
|
1668
|
+
<button class="btn" style="margin-top:12px" onclick="gotoStep('select',false)">去选片</button></div>
|
|
1669
|
+
</div>`;
|
|
1670
|
+
wireInspector();
|
|
1671
|
+
wireDeck();
|
|
1672
|
+
wireDeckControls();
|
|
1673
|
+
if (S.outputs[S.step] && !S.outputSlots[S.step]) renderAiOut(S.outputs[S.step], S.step, null);
|
|
1674
|
+
return;
|
|
1572
1675
|
}
|
|
1573
1676
|
const g = S.data.grade, meta = g.meta;
|
|
1574
1677
|
const dirty = (k) => Math.abs((S.params[k] || 0) - meta[k].default) > 1e-9;
|
|
@@ -1659,41 +1762,33 @@ function renderInspector(passive = true) {
|
|
|
1659
1762
|
</button>`;
|
|
1660
1763
|
}).join("");
|
|
1661
1764
|
|
|
1662
|
-
const picked = new Set((S.data.slots || []).filter((x) => x.present).map((x) => x.source));
|
|
1663
|
-
const selectPane = `
|
|
1664
|
-
|
|
1665
|
-
<div class="
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
<div class="note"
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
<
|
|
1686
|
-
黄金位法则:1号位抓眼开篇、5号位C位核心主角、9号位余韵收尾。</div>
|
|
1687
|
-
<div class="row" style="margin-top:9px">
|
|
1688
|
-
<button class="btn btn-sm" onclick="storyOrder('time')">按时间预演</button>
|
|
1689
|
-
<button class="btn btn-sm" onclick="storyOrder('place')">按地点预演</button>
|
|
1690
|
-
</div><div id="story-preview"></div>
|
|
1691
|
-
<button class="btn btn-sm btn-ai" style="width:100%;margin-top:9px" onclick="aiRun('pick')">
|
|
1765
|
+
const picked = new Set((S.data.slots || []).filter((x) => x.present).map((x) => x.source));
|
|
1766
|
+
const selectPane = `
|
|
1767
|
+
${actionDeckHtml({ chips: `
|
|
1768
|
+
<div class="note" style="margin-bottom:7px">选片决定全篇气质。已启用人物去重与僵硬摆拍扣分算法;手动换图不需要调用 AI。</div>
|
|
1769
|
+
<div class="pref-grid">${prefChips}</div>`, prompt: promptBox })}
|
|
1770
|
+
|
|
1771
|
+
<div class="card">
|
|
1772
|
+
<div class="card-h"><span class="t">选片状态</span><span class="sp"></span>
|
|
1773
|
+
<span class="s">${picked.size} / 9 已就位</span></div>
|
|
1774
|
+
<div class="note" style="margin-bottom:9px">
|
|
1775
|
+
照片统一在左侧九宫格与候选池操作,不在这里重复展示。原始文件不会被修改。
|
|
1776
|
+
</div>
|
|
1777
|
+
<button class="btn btn-sm" onclick="focusPhotoLibrary()">前往照片库</button>
|
|
1778
|
+
</div>
|
|
1779
|
+
|
|
1780
|
+
<div class="card">
|
|
1781
|
+
<div class="card-h"><span class="t">摆放顺序与节奏</span></div>
|
|
1782
|
+
<div class="note">左侧九宫格支持直接拖动换位。
|
|
1783
|
+
黄金位法则:1号位抓眼开篇、5号位C位核心主角、9号位余韵收尾。</div>
|
|
1784
|
+
<div class="row" style="margin-top:9px">
|
|
1785
|
+
<button class="btn btn-sm" onclick="storyOrder('time')">按时间预演</button>
|
|
1786
|
+
<button class="btn btn-sm" onclick="storyOrder('place')">按地点预演</button>
|
|
1787
|
+
</div><div id="story-preview"></div>
|
|
1788
|
+
<button class="btn btn-sm btn-ai" style="width:100%;margin-top:9px" onclick="aiRun('pick')">
|
|
1692
1789
|
仅为当前已选照片建议排序
|
|
1693
|
-
</button>
|
|
1694
|
-
</div
|
|
1695
|
-
${promptBox}
|
|
1696
|
-
<div class="ai-out"></div>`;
|
|
1790
|
+
</button>
|
|
1791
|
+
</div>`;
|
|
1697
1792
|
|
|
1698
1793
|
const stepMeta = STEP_BY_ID[S.step] || STEPS[0];
|
|
1699
1794
|
const stepIdx = STEPS.findIndex((x) => x.id === stepMeta.id);
|
|
@@ -1729,123 +1824,102 @@ function renderInspector(passive = true) {
|
|
|
1729
1824
|
<div class="note">确认照片与文案后再打包;未完成的步骤可以随时返回调整。</div>
|
|
1730
1825
|
</div>`;
|
|
1731
1826
|
|
|
1732
|
-
box.innerHTML = `
|
|
1733
|
-
${stepHead}
|
|
1734
|
-
${workflowControlsHtml()}
|
|
1827
|
+
box.innerHTML = `
|
|
1828
|
+
${stepHead}
|
|
1735
1829
|
<div class="step-panes">
|
|
1736
1830
|
${pane("select", selectPane)}
|
|
1737
|
-
${pane("crop", `
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
<p class="note" style="margin-bottom:8px"
|
|
1743
|
-
<div class="ratio-grid">${ratioCards}</div>
|
|
1744
|
-
<div class="note" style="margin-top:11px">
|
|
1745
|
-
<b>裁切重心</b>:把照片裁成上面选的画幅时,保留画面里的哪一块。
|
|
1746
|
-
点下面的小图,或点「点选裁切重心」后在画布上点一下主体位置。
|
|
1747
|
-
</div>
|
|
1748
|
-
<div class="aim-grid" id="aim-grid">
|
|
1749
|
-
<i class="v" style="left:33.333%"></i><i class="v" style="left:66.666%"></i>
|
|
1750
|
-
<i class="h" style="top:33.333%"></i><i class="h" style="top:66.666%"></i>
|
|
1751
|
-
<span class="aim-dot" id="aim-dot" style="left:${s.anchor[0] * 100}%;top:${s.anchor[1] * 100}%"></span>
|
|
1752
|
-
</div>
|
|
1753
|
-
</div>
|
|
1754
|
-
<div class="card" id="insp-info">
|
|
1755
|
-
<div class="slot-head">
|
|
1756
|
-
<span class="no">${String(s.slot).padStart(2, "0")}</span>
|
|
1757
|
-
<div><div class="lb">${H(s.label || "精选位")}</div>
|
|
1758
|
-
<div class="note" style="font-size:10px">${H(s.source)}</div></div>
|
|
1759
|
-
<div class="spacer" style="flex:1"></div>
|
|
1760
|
-
</div>
|
|
1761
|
-
<dl class="kv">
|
|
1762
|
-
${s.scene ? `<dt>场景</dt><dd>${H(s.scene)}</dd>` : ""}
|
|
1763
|
-
${s.note ? `<dt>点评</dt><dd>${H(s.note)}</dd>` : ""}
|
|
1764
|
-
${s.score != null ? `<dt>选片分</dt><dd>${(s.score * 10).toFixed(1)} / 10</dd>` : ""}
|
|
1765
|
-
${s.color ? `<dt>主调</dt><dd><span class="swatch" style="background:${H(s.color)}"></span>${H(s.color)}</dd>` : ""}
|
|
1766
|
-
</dl>
|
|
1767
|
-
${(s.reasons || []).length ? `<div class="chips" style="margin-top:9px">
|
|
1768
|
-
${s.reasons.map((r) => `<span class="chip">${H(r)}</span>`).join("")}</div>` : ""}
|
|
1769
|
-
${s.ai && s.ai.scene ? `<div class="note" style="margin-top:9px">AI 判断:${H(s.ai.scene)}</div>` : ""}
|
|
1770
|
-
</div
|
|
1771
|
-
${
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
<button class="btn btn-sm ghost" onclick="resetGrade()">归零</button></div>
|
|
1831
|
+
${pane("crop", `
|
|
1832
|
+
${actionDeckHtml({ chips: deckChipsHtml(), result: candHtml, prompt: promptBox })}
|
|
1833
|
+
<div class="card" id="insp-frame">
|
|
1834
|
+
<div class="card-h"><span class="t">手动画幅与裁切重心</span><span class="sp"></span>
|
|
1835
|
+
<span class="s">${s.ratio ? s.ratio[0] + ":" + s.ratio[1] : "原图"}</span></div>
|
|
1836
|
+
<p class="note" style="margin-bottom:8px">点下面的比例或重心即生效,不需要点操作台的按钮,可在画布工具栏撤回。</p>
|
|
1837
|
+
<div class="ratio-grid">${ratioCards}</div>
|
|
1838
|
+
<div class="note" style="margin-top:11px">
|
|
1839
|
+
<b>裁切重心</b>:把照片裁成上面选的画幅时,保留画面里的哪一块。
|
|
1840
|
+
点下面的小图,或点「点选裁切重心」后在画布上点一下主体位置。
|
|
1841
|
+
</div>
|
|
1842
|
+
<div class="aim-grid" id="aim-grid">
|
|
1843
|
+
<i class="v" style="left:33.333%"></i><i class="v" style="left:66.666%"></i>
|
|
1844
|
+
<i class="h" style="top:33.333%"></i><i class="h" style="top:66.666%"></i>
|
|
1845
|
+
<span class="aim-dot" id="aim-dot" style="left:${s.anchor[0] * 100}%;top:${s.anchor[1] * 100}%"></span>
|
|
1846
|
+
</div>
|
|
1847
|
+
</div>
|
|
1848
|
+
<div class="card" id="insp-info">
|
|
1849
|
+
<div class="slot-head">
|
|
1850
|
+
<span class="no">${String(s.slot).padStart(2, "0")}</span>
|
|
1851
|
+
<div><div class="lb">${H(s.label || "精选位")}</div>
|
|
1852
|
+
<div class="note" style="font-size:10px">${H(s.source)}</div></div>
|
|
1853
|
+
<div class="spacer" style="flex:1"></div>
|
|
1854
|
+
</div>
|
|
1855
|
+
<dl class="kv">
|
|
1856
|
+
${s.scene ? `<dt>场景</dt><dd>${H(s.scene)}</dd>` : ""}
|
|
1857
|
+
${s.note ? `<dt>点评</dt><dd>${H(s.note)}</dd>` : ""}
|
|
1858
|
+
${s.score != null ? `<dt>选片分</dt><dd>${(s.score * 10).toFixed(1)} / 10</dd>` : ""}
|
|
1859
|
+
${s.color ? `<dt>主调</dt><dd><span class="swatch" style="background:${H(s.color)}"></span>${H(s.color)}</dd>` : ""}
|
|
1860
|
+
</dl>
|
|
1861
|
+
${(s.reasons || []).length ? `<div class="chips" style="margin-top:9px">
|
|
1862
|
+
${s.reasons.map((r) => `<span class="chip">${H(r)}</span>`).join("")}</div>` : ""}
|
|
1863
|
+
${s.ai && s.ai.scene ? `<div class="note" style="margin-top:9px">AI 判断:${H(s.ai.scene)}</div>` : ""}
|
|
1864
|
+
</div>`)}
|
|
1865
|
+
${pane("grade", `
|
|
1866
|
+
${actionDeckHtml({ chips: deckChipsHtml(), result: candHtml, prompt: promptBox })}
|
|
1867
|
+
<div class="card" id="insp-grade">
|
|
1868
|
+
<div class="card-h"><span class="t">手动胶片预设与影调</span><span class="sp"></span>
|
|
1869
|
+
<button class="btn btn-sm ghost" onclick="resetGrade()">归零</button></div>
|
|
1777
1870
|
<div class="preset-grid">${presets}</div>
|
|
1778
1871
|
<details id="grade-adjustments" ${S.gradeAdjustOpen ? "open" : ""}>
|
|
1779
1872
|
<summary class="note" style="cursor:pointer;margin-top:12px">手动微调 · ${g.order.length} 项</summary>
|
|
1780
1873
|
<div class="sliders">
|
|
1781
|
-
<div class="grp">影调</div>${group("影调")}
|
|
1782
|
-
<div class="grp">色彩</div>${group("色彩")}
|
|
1783
|
-
<div class="grp">质感</div>${group("质感")}
|
|
1874
|
+
<div class="grp">影调</div>${group("影调")}
|
|
1875
|
+
<div class="grp">色彩</div>${group("色彩")}
|
|
1876
|
+
<div class="grp">质感</div>${group("质感")}
|
|
1784
1877
|
</div>
|
|
1785
1878
|
</details>
|
|
1786
|
-
<div class="note" id="grade-summary" style="margin-top:8px">${H(s.grade_summary || "")}</div>
|
|
1787
|
-
</div
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
${promptBox}
|
|
1796
|
-
<div class="
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
<div class="
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
${imageSizeControlsHtml("
|
|
1807
|
-
</div>
|
|
1808
|
-
${editResultCard(s)}
|
|
1809
|
-
${promptBox}
|
|
1810
|
-
<div class="ai-out"></div>`)}
|
|
1811
|
-
${pane("finish", `
|
|
1812
|
-
<div class="ai-out"></div>
|
|
1813
|
-
<div class="card" id="insp-finish">
|
|
1814
|
-
<div class="card-h"><span class="t">出品台</span><span class="sp"></span>
|
|
1815
|
-
<span class="s">使用当前精修成品</span></div>
|
|
1816
|
-
${finishSourcesHtml()}
|
|
1879
|
+
<div class="note" id="grade-summary" style="margin-top:8px">${H(s.grade_summary || "")}</div>
|
|
1880
|
+
</div>`)}
|
|
1881
|
+
${pane("retouch", `
|
|
1882
|
+
${actionDeckHtml({ chips: `
|
|
1883
|
+
<div class="note" style="margin-bottom:7px">点预设只会把指令填进下面的框,<b>不会自动开修</b>;确认后再点「开始修图」。重绘像素,需人工验收。</div>
|
|
1884
|
+
<div class="row" style="margin-bottom:8px">${editPresets}</div>`,
|
|
1885
|
+
result: editResultCard(s), prompt: promptBox })}
|
|
1886
|
+
${imageSizeControlsHtml("edit")}`)}
|
|
1887
|
+
${pane("finish", `
|
|
1888
|
+
${actionDeckHtml({ chips: finishSourcesHtml(), result: `<div id="finish-out"></div>`, prompt: promptBox })}
|
|
1889
|
+
<div class="card" id="insp-art">
|
|
1890
|
+
<div class="card-h"><span class="t">AI 艺术出片</span><span class="sp"></span>
|
|
1891
|
+
<span class="s">选风格 → 点操作台的「生成所选作品」</span></div>
|
|
1892
|
+
<div class="note" style="margin-bottom:9px">
|
|
1893
|
+
选择一种风格并填写可选标题,再点操作台的「生成所选作品」。
|
|
1894
|
+
</div>
|
|
1895
|
+
<div class="art-grid" style="grid-template-columns:1fr 1fr">${artCards}</div>
|
|
1896
|
+
<div class="row" style="margin-top:9px">
|
|
1897
|
+
<input id="art-title" data-output-text value="${H(S.artTitle)}" placeholder="可选:指定标题 / 短句(留空则由照片决定)" style="flex:1;min-width:0;padding:9px;border:1px solid var(--line-2);border-radius:6px">
|
|
1898
|
+
</div>
|
|
1899
|
+
${imageSizeControlsHtml("art")}
|
|
1817
1900
|
<label style="display:flex;align-items:center;gap:8px;margin:14px 0 6px;cursor:pointer">
|
|
1818
1901
|
<input id="output-no-text" type="checkbox" ${S.data.preferences?.no_text ? "checked" : ""} onchange="setOutputNoText(this.checked)">
|
|
1819
1902
|
<b>不加文字</b>
|
|
1820
1903
|
</label>
|
|
1821
|
-
<p class="note"
|
|
1822
|
-
<div class="sec-title" style="margin-top:16px">AI 艺术出片</div>
|
|
1823
|
-
<div class="note" style="margin-bottom:9px">
|
|
1824
|
-
选择一种风格并填写可选标题,再生成这一张衍生作品。AI 来源遵循本页顶部选择。
|
|
1825
|
-
</div>
|
|
1826
|
-
<div class="art-grid" style="grid-template-columns:1fr 1fr">${artCards}</div>
|
|
1827
|
-
<div class="row" style="margin-top:9px">
|
|
1828
|
-
<input id="art-title" data-output-text value="${H(S.artTitle)}" placeholder="可选:指定标题 / 短句(留空则由照片决定)" style="flex:1;min-width:0;padding:9px;border:1px solid var(--line-2);border-radius:6px">
|
|
1829
|
-
</div>
|
|
1830
|
-
${imageSizeControlsHtml("art")}
|
|
1831
|
-
<div class="sec-title" style="margin-top:16px">本地画板<span class="n">即刻生成 · 不调用模型</span></div>
|
|
1832
|
-
<select id="local-product" aria-label="本地衍生品类型" style="width:100%;padding:8px;border:1px solid var(--line-2);border-radius:6px">
|
|
1833
|
-
${[["postcard","手撕纸明信片"],["watercolor","写意水墨水彩"],["zine","Zine 纸刊"],["poster","杂志海报"],
|
|
1834
|
-
["film","电影剧照卡"],["polaroid","复古拍立得"],["grid9","仅生成九宫格"],["map","高德足迹地图"]].map(([k,t]) =>
|
|
1835
|
-
`<option value="${k}" ${(S.localProduct || "postcard") === k ? "selected" : ""}>${t}</option>`).join("")}
|
|
1836
|
-
</select>
|
|
1837
|
-
<div class="row" style="margin-top:8px">
|
|
1838
|
-
<button class="btn btn-sm ghost" onclick="openProducts()">标题与排版设置</button></div>
|
|
1839
|
-
<p class="note" id="local-product-note" style="margin-top:7px"></p>
|
|
1840
|
-
<div class="sec-title" style="margin-top:16px">已生成</div>
|
|
1841
|
-
<div id="finish-out" style="margin-top:9px"></div>
|
|
1842
|
-
</div>`)}
|
|
1843
|
-
${pane("caption", `
|
|
1844
|
-
<div class="card" id="insp-caption">
|
|
1845
|
-
<div class="card-h"><span class="t">朋友圈文案与标题</span></div>
|
|
1846
|
-
<p class="note" style="margin-bottom:10px">点击上方「生成 3 版文案」。生成后可逐条复制;作品标题在出片页的排版设置中管理。</p>
|
|
1847
|
-
<div id="captions"></div>
|
|
1904
|
+
<p class="note">新生成的衍生作品不添加标题、字幕、日期或装饰字;原照片和地图底图自带的文字不擦除。已有作品需重新生成。</p>
|
|
1848
1905
|
</div>
|
|
1906
|
+
<div class="card" id="insp-local">
|
|
1907
|
+
<details>
|
|
1908
|
+
<summary class="card-h" style="cursor:pointer;display:flex;align-items:center">
|
|
1909
|
+
<span class="t">本地画板</span><span class="sp" style="flex:1"></span>
|
|
1910
|
+
<span class="s">即刻生成 · 不调用模型</span></summary>
|
|
1911
|
+
<select id="local-product" aria-label="本地衍生品类型" style="width:100%;margin-top:9px;padding:8px;border:1px solid var(--line-2);border-radius:6px">
|
|
1912
|
+
${[["postcard","手撕纸明信片"],["watercolor","写意水墨水彩"],["zine","Zine 纸刊"],["poster","杂志海报"],
|
|
1913
|
+
["film","电影剧照卡"],["polaroid","复古拍立得"],["grid9","仅生成九宫格"],["map","高德足迹地图"]].map(([k,t]) =>
|
|
1914
|
+
`<option value="${k}" ${(S.localProduct || "postcard") === k ? "selected" : ""}>${t}</option>`).join("")}
|
|
1915
|
+
</select>
|
|
1916
|
+
<div class="row" style="margin-top:8px">
|
|
1917
|
+
<button class="btn btn-sm ghost" onclick="openProducts()">标题与排版设置</button></div>
|
|
1918
|
+
<p class="note" id="local-product-note" style="margin-top:7px"></p>
|
|
1919
|
+
</details>
|
|
1920
|
+
</div>`)}
|
|
1921
|
+
${pane("caption", `
|
|
1922
|
+
${actionDeckHtml({ result: `<div id="captions"></div>`, prompt: promptBox })}
|
|
1849
1923
|
<div class="card" id="insp-publish">
|
|
1850
1924
|
<div class="card-h"><span class="t">确认后打包</span><span class="s">只保存到本机</span></div>
|
|
1851
1925
|
<label style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
|
|
@@ -1858,23 +1932,22 @@ function renderInspector(passive = true) {
|
|
|
1858
1932
|
<p>检查导出 JPEG 是否还含 EXIF 定位;地图上的可见地点需另行确认。</p>
|
|
1859
1933
|
<button class="btn btn-sm" onclick="checkExportPrivacy()">检查导出包</button>
|
|
1860
1934
|
<div id="privacy-result" class="note" role="status"></div></div>
|
|
1935
|
+
</div>`)}
|
|
1861
1936
|
</div>
|
|
1862
|
-
${
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
output.innerHTML = S.outputs[S.step];
|
|
1877
|
-
box.scrollTop = scroll;
|
|
1937
|
+
${stepFoot}`;
|
|
1938
|
+
|
|
1939
|
+
wireInspector();
|
|
1940
|
+
wireDeck();
|
|
1941
|
+
wireDeckControls();
|
|
1942
|
+
wireCopilot();
|
|
1943
|
+
renderCaptions();
|
|
1944
|
+
syncExecutor();
|
|
1945
|
+
renderFinishPane();
|
|
1946
|
+
const output = outBox();
|
|
1947
|
+
if (output && S.outputs[S.step] && (!S.outputSlots[S.step] || S.outputSlots[S.step] === S.sel))
|
|
1948
|
+
output.innerHTML = S.outputs[S.step];
|
|
1949
|
+
updateLocalProductState();
|
|
1950
|
+
box.scrollTop = scroll;
|
|
1878
1951
|
}
|
|
1879
1952
|
|
|
1880
1953
|
|
|
@@ -1915,11 +1988,7 @@ function wireInspector() {
|
|
|
1915
1988
|
if (size) size.addEventListener("change", () => { S[kind + "Size"] = size.value; });
|
|
1916
1989
|
}
|
|
1917
1990
|
updateLocalProductState();
|
|
1918
|
-
|
|
1919
|
-
if (inst) inst.addEventListener("input", () => {
|
|
1920
|
-
S.editInstruction = inst.value;
|
|
1921
|
-
rememberEditDraft();
|
|
1922
|
-
});
|
|
1991
|
+
// The 修图指令 box is the deck's #deck-intent now; wireDeck() binds it once.
|
|
1923
1992
|
const title = $("art-title");
|
|
1924
1993
|
if (title) title.addEventListener("input", () => { S.artTitle = title.value; });
|
|
1925
1994
|
const promptBox = $("prompt-box");
|
|
@@ -1977,9 +2046,12 @@ function wireInspector() {
|
|
|
1977
2046
|
el.addEventListener("click", () => {
|
|
1978
2047
|
const p = (S.data.ai.edit_presets || []).find((x) => x.key === el.dataset.edit);
|
|
1979
2048
|
S.editPreset = el.dataset.edit;
|
|
1980
|
-
S.editInstruction = p ? p.instruction : "";
|
|
1981
|
-
|
|
1982
|
-
|
|
2049
|
+
S.editInstruction = p ? p.instruction : "";
|
|
2050
|
+
S.copilotIntent = S.editInstruction;
|
|
2051
|
+
(S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = S.editInstruction;
|
|
2052
|
+
rememberEditDraft();
|
|
2053
|
+
const box = $("deck-intent");
|
|
2054
|
+
if (box) box.value = S.editInstruction;
|
|
1983
2055
|
document.querySelectorAll("[data-edit]").forEach((b) => b.classList.remove("on"));
|
|
1984
2056
|
el.classList.add("on");
|
|
1985
2057
|
});
|
|
@@ -2015,8 +2087,8 @@ function renderCaptions() {
|
|
|
2015
2087
|
const c = S.data.captions || {};
|
|
2016
2088
|
const vs = c.versions || [];
|
|
2017
2089
|
if (!vs.length) {
|
|
2018
|
-
box.innerHTML = `<div class="note"
|
|
2019
|
-
|
|
2090
|
+
box.innerHTML = `<div class="note">还没有文案。点操作台的「生成 3 版文案」,模型会依据选片时记录的真实场景写 3 版,
|
|
2091
|
+
不会像旧版那样塞一段和照片无关的模板文字。(这一帧的 Agent 轨道会把文案直接登记到这里。)</div>`;
|
|
2020
2092
|
return;
|
|
2021
2093
|
}
|
|
2022
2094
|
box.innerHTML = (c.narrative ? `<div class="note" style="margin-bottom:10px;font-style:italic">
|
|
@@ -2837,7 +2909,7 @@ function promptCtx(action, opts) {
|
|
|
2837
2909
|
if (action === "edit") {
|
|
2838
2910
|
ctx.preset = S.editPreset || "";
|
|
2839
2911
|
ctx.image_size = S.editSize || "auto";
|
|
2840
|
-
const inst = $("
|
|
2912
|
+
const inst = $("deck-intent");
|
|
2841
2913
|
if (inst && inst.value.trim()) ctx.instruction = inst.value.trim();
|
|
2842
2914
|
}
|
|
2843
2915
|
if (s) ctx.photo = s.source;
|
|
@@ -2963,39 +3035,47 @@ async function aiTitle() {
|
|
|
2963
3035
|
await aiRun("title");
|
|
2964
3036
|
}
|
|
2965
3037
|
|
|
2966
|
-
async function aiEdit() {
|
|
2967
|
-
|
|
2968
|
-
const
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
S.
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3038
|
+
async function aiEdit() {
|
|
3039
|
+
// The 修图指令 input IS the deck's input now (one box per step, not two).
|
|
3040
|
+
const inst = $("deck-intent");
|
|
3041
|
+
const text = inst ? inst.value.trim() : (S.editInstruction || "");
|
|
3042
|
+
if (!text) { toast("先写一句修图指令,或点一个预设按钮", "err"); return; }
|
|
3043
|
+
S.editInstruction = text;
|
|
3044
|
+
S.copilotIntent = text;
|
|
3045
|
+
rememberEditDraft();
|
|
3046
|
+
await aiRun("edit");
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
function fillInstruction() {
|
|
3050
|
+
const p = (S.data.ai.edit_presets || []).find((x) => x.key === S.editPreset);
|
|
3051
|
+
if (!p) { toast("先在左侧点一个修图预设", "err"); return; }
|
|
3052
|
+
S.editInstruction = p.instruction;
|
|
3053
|
+
S.copilotIntent = p.instruction;
|
|
3054
|
+
(S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = p.instruction;
|
|
3055
|
+
const box = $("deck-intent");
|
|
3056
|
+
if (box) box.value = p.instruction;
|
|
3057
|
+
rememberEditDraft();
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
function outBox() {
|
|
3061
|
+
// One output region PER STEP, now living inside that step's action deck. All six
|
|
3062
|
+
// panes once shared a single duplicate `ai-out` id, so getElementById() always
|
|
3063
|
+
// returned the SELECT pane's box: a result produced on 调色/修图/出片/文案 was
|
|
3064
|
+
// painted into a hidden pane and the user saw nothing at all.
|
|
3065
|
+
const step = S.step || "";
|
|
3066
|
+
return document.getElementById(`ai-out-${step}`)
|
|
3067
|
+
|| document.querySelector(`[data-pane="${step}"] .ai-out`)
|
|
3068
|
+
|| document.querySelector(".ai-out");
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
function renderAiOut(html, step = S.step, slot = ["grade", "crop", "retouch"].includes(step) ? S.sel : null) {
|
|
3072
|
+
S.outputs[step] = html;
|
|
3073
|
+
S.outputSlots[step] = slot;
|
|
3074
|
+
// No model-source guard: a result is a result. Skipping the paint on the
|
|
3075
|
+
// agent-model track is why 「Agent 说得挺好,页面上什么都没有」 was possible.
|
|
3076
|
+
if (step !== S.step || (slot && slot !== S.sel)) return;
|
|
3077
|
+
const box = outBox();
|
|
3078
|
+
if (box) box.innerHTML = html;
|
|
2999
3079
|
}
|
|
3000
3080
|
|
|
3001
3081
|
/* ---------------- executor switch ---------------- */
|
|
@@ -3186,7 +3266,7 @@ function renderConfigPanels(c) {
|
|
|
3186
3266
|
|
|
3187
3267
|
$("models").innerHTML = models.length
|
|
3188
3268
|
? models.map((m) => `<span class="chip" style="font-family:var(--mono)">${H(m)}</span>`).join("")
|
|
3189
|
-
: `<span class="note"
|
|
3269
|
+
: `<span class="note">还没探测过。点「探测连通性与在售模型」拉取真实列表。</span>`;
|
|
3190
3270
|
|
|
3191
3271
|
const cm = c.custom_models || {};
|
|
3192
3272
|
const keys = Object.keys(cm);
|
package/README.md
CHANGED
|
@@ -181,10 +181,15 @@ trip-pulse/
|
|
|
181
181
|
- 出片页明确显示「原片处理版 / 已采用修图」。存在待采用修图时,先要求采用或拒绝,
|
|
182
182
|
不再静默使用原片生成。采用后仍需重新生成海报/拼图或重新打包;旧产物不会自动更新。
|
|
183
183
|
|
|
184
|
-
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
- 每一步的主流程收进同一张「本步操作台」:谁来做/范围 → 要求输入 → 触发按钮 →
|
|
185
|
+
本轮提示词或发给 Agent 的简报 → 结果(含候选与 Agent 会话),自上而下一次完成,
|
|
186
|
+
不再需要「在下面写要求、回顶部点按钮、再滚下去看结果」。手动控件(预设、微调、
|
|
187
|
+
画幅、风格)降到操作台下方,只作手动替代路径。
|
|
188
|
+
- 要求只用一个输入框:网关轨道它是发出去的要求,Agent 轨道同一个框就是会话追问;
|
|
189
|
+
裁切/调色用「当前格 / 全部已选」切换范围。方向预设只填写要求,不会自动调用模型。
|
|
190
|
+
- 修图页同一套流程:预设只填指令 → 「开始修图」 → 下方结果卡直接采用/拒绝。
|
|
191
|
+
- 出片页拆成四块:操作台(出片来源 + 触发 + 结果图库)、AI 艺术出片、本地画板(折叠)、
|
|
192
|
+
打包仍在文案步确认之后。
|
|
188
193
|
- 全组调色支持先预览再采用;过期配方及已取消网关任务的返回结果不再自动落盘。
|
|
189
194
|
Agent 的取消会转发停止请求;它在取消前已执行的修改仍需按需撤回。
|
|
190
195
|
- 新采用的修图版本保存稳定副本;新的待采用结果与当前已采用版本分开,可明确采用或拒绝。
|
package/bin/install.js
CHANGED