会话与前端接上听书、呈现投影、请求轮廓和旁观气泡偏好。
新建作品可选听书连载,朗读与消费进度走专用接口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
## 它能做什么
|
## 它能做什么
|
||||||
|
|
||||||
- **创作**:为新作品选进料方式(工序编排 / 对话落盘)与配方,由编排器调度技能,落成可复用的运行规格与开场
|
- **创作**:为新作品选进料方式(工序编排 / 对话落盘(默认选项))与配方,由编排器调度技能,落成可复用的运行规格与开场
|
||||||
- **游玩**:在钉住的规格下回合推进;可切换呈现壳,保存游玩进度
|
- **游玩**:在钉住的规格下回合推进;可切换呈现壳,保存游玩进度
|
||||||
- **技能包**:能力以磁盘上的技能包装载(当前主包为「世界模拟器」相关配方与模块)
|
- **技能包**:能力以磁盘上的技能包装载(当前主包为「世界模拟器」相关配方与模块)(目前不需要手动选择)
|
||||||
- **预设与库**:兼容 SillyTavern 风格的提示词预设;文风库 / 偏好库可注入创作节点,种子可从仓库恢复
|
- **预设与库**:兼容 SillyTavern 风格的提示词预设;文风库 / 偏好库可注入创作节点,种子可从仓库恢复
|
||||||
- **多接口**:分组管理 API、嗅探型号、切换同一接口下的模型
|
- **多接口**:分组管理 API、嗅探型号、切换同一接口下的模型
|
||||||
- **本机优先**:Web(`localhost:23337`)或 `start-app.bat` 桌面壳;运维可用 `console.bat`
|
- **本机优先**:Web(`localhost:23337`)或 `start-app.bat` 桌面壳;运维可用 `console.bat`
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
1. 安装 [Node.js](https://nodejs.org/)(含 npm)
|
1. 安装 [Node.js](https://nodejs.org/)(含 npm)
|
||||||
2. 双击 `start.bat`(桌面窗口则用 `start-app.bat`),浏览器打开 http://localhost:23337
|
2. 双击 `start.bat`(桌面窗口则用 `start-app.bat`),浏览器打开 [http://localhost:23337](http://localhost:23337)
|
||||||
3. 在 **设置** 里新增模型接口(Base URL、API Key、型号);根目录已有 `.env` 时可跳过
|
3. 在 **设置** 里新增模型接口(Base URL、API Key、型号);根目录已有 `.env` 时可跳过
|
||||||
4. **新建作品** → 对话写出一张简单可玩卡 → **保存定稿** → 左侧菜单选 **游玩**
|
4. **新建作品** → 对话写出一张简单可玩卡 → **保存定稿** → 左侧菜单选 **游玩**
|
||||||
|
|
||||||
@@ -31,12 +31,13 @@
|
|||||||
→ 游玩(执行单元按规格上场)→ 保存游玩
|
→ 游玩(执行单元按规格上场)→ 保存游玩
|
||||||
```
|
```
|
||||||
|
|
||||||
术语以 [`docs/ui-glossary.md`](docs/ui-glossary.md) 为准(进料方式、配方、编排器、技能、运行规格、执行单元)。
|
术语以 `[docs/ui-glossary.md](docs/ui-glossary.md)` 为准(进料方式、配方、编排器、技能、运行规格、执行单元)。
|
||||||
|
|
||||||
## 仓库一览
|
## 仓库一览
|
||||||
|
|
||||||
|
|
||||||
| 路径 | 用途 |
|
| 路径 | 用途 |
|
||||||
|------|------|
|
| ------------------- | ------------------------------ |
|
||||||
| `src/` | 运行时、会话、技能装载、Book/预设/库、HTTP API |
|
| `src/` | 运行时、会话、技能装载、Book/预设/库、HTTP API |
|
||||||
| `web/` | 前端静态页 |
|
| `web/` | 前端静态页 |
|
||||||
| `skills/` | 技能包(见 `skills/README.md`) |
|
| `skills/` | 技能包(见 `skills/README.md`) |
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
| `docs/` | 架构与契约文档 |
|
| `docs/` | 架构与契约文档 |
|
||||||
| `tests/` | Vitest |
|
| `tests/` | Vitest |
|
||||||
|
|
||||||
|
|
||||||
用户数据目录可用环境变量 `WRITING_AGENT_DATA_DIR` 覆盖;默认 `~/.writing-agent`(Windows 多为 `%USERPROFILE%\.writing-agent`)。
|
用户数据目录可用环境变量 `WRITING_AGENT_DATA_DIR` 覆盖;默认 `~/.writing-agent`(Windows 多为 `%USERPROFILE%\.writing-agent`)。
|
||||||
|
|
||||||
## 维护者
|
## 维护者
|
||||||
@@ -67,8 +69,9 @@ Windows 运维脚本:`console.bat`(查询 / 启停 / 日志)。入口实
|
|||||||
|
|
||||||
### 文档索引
|
### 文档索引
|
||||||
|
|
||||||
|
|
||||||
| 文档 | 内容 |
|
| 文档 | 内容 |
|
||||||
|------|------|
|
| --------------------------------------------------------------- | -------------------- |
|
||||||
| [architecture.md](docs/architecture.md) | 系统架构与模块边界 |
|
| [architecture.md](docs/architecture.md) | 系统架构与模块边界 |
|
||||||
| [ui-glossary.md](docs/ui-glossary.md) | 用户可见术语 |
|
| [ui-glossary.md](docs/ui-glossary.md) | 用户可见术语 |
|
||||||
| [implementation-guide.md](docs/implementation-guide.md) | 文档地图与实现约定 |
|
| [implementation-guide.md](docs/implementation-guide.md) | 文档地图与实现约定 |
|
||||||
@@ -81,8 +84,9 @@ Windows 运维脚本:`console.bat`(查询 / 启停 / 日志)。入口实
|
|||||||
| [px-roadmap.md](docs/px-roadmap.md) | 产品路线图 |
|
| [px-roadmap.md](docs/px-roadmap.md) | 产品路线图 |
|
||||||
| [skills/README.md](skills/README.md) | 技能包目录说明 |
|
| [skills/README.md](skills/README.md) | 技能包目录说明 |
|
||||||
|
|
||||||
|
|
||||||
`docs/book-storage.md` 描述的是目标存储模型,**不是**当前磁盘布局;当前以 Book JSON + session + run-snapshots 为准。
|
`docs/book-storage.md` 描述的是目标存储模型,**不是**当前磁盘布局;当前以 Book JSON + session + run-snapshots 为准。
|
||||||
|
|
||||||
## 许可与第三方
|
## 许可与第三方
|
||||||
|
|
||||||
见 [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。项目版本见 `package.json`(当前 `0.1.0`,`private`)。
|
见 `[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)`。项目版本见 `package.json`(当前 `0.1.0`,`private`)。
|
||||||
+26
-3
@@ -9,7 +9,20 @@ import {
|
|||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { BookProject, BookSummary } from "../types/book.js";
|
import type {
|
||||||
|
BookCreationMode,
|
||||||
|
BookProject,
|
||||||
|
BookSummary,
|
||||||
|
} from "../types/book.js";
|
||||||
|
|
||||||
|
function normalizeBookCreationMode(
|
||||||
|
raw: string | undefined | null,
|
||||||
|
): BookCreationMode {
|
||||||
|
if (raw === "listen_serial" || raw === "recipe" || raw === "dictate") {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
return "dictate";
|
||||||
|
}
|
||||||
import type { PersistedBookSession } from "../types/book-session.js";
|
import type { PersistedBookSession } from "../types/book-session.js";
|
||||||
import type { RunSnapshot } from "../types/run-snapshot.js";
|
import type { RunSnapshot } from "../types/run-snapshot.js";
|
||||||
import { ensureUserDataDirs, getUserDataDir } from "../config/user-data-dir.js";
|
import { ensureUserDataDirs, getUserDataDir } from "../config/user-data-dir.js";
|
||||||
@@ -57,6 +70,7 @@ export function listBooks(): BookSummary[] {
|
|||||||
title: b.title,
|
title: b.title,
|
||||||
activeSkillId: b.activeSkillId ?? b.orchestratorId,
|
activeSkillId: b.activeSkillId ?? b.orchestratorId,
|
||||||
activeSkillName: b.activeSkillName ?? b.orchestratorName,
|
activeSkillName: b.activeSkillName ?? b.orchestratorName,
|
||||||
|
creationMode: normalizeBookCreationMode(b.creationMode),
|
||||||
preview: b.preview,
|
preview: b.preview,
|
||||||
updatedAt: b.updatedAt,
|
updatedAt: b.updatedAt,
|
||||||
orchestratorId: b.orchestratorId,
|
orchestratorId: b.orchestratorId,
|
||||||
@@ -67,19 +81,27 @@ export function listBooks(): BookSummary[] {
|
|||||||
export function getBook(id: string): BookProject | null {
|
export function getBook(id: string): BookProject | null {
|
||||||
try {
|
try {
|
||||||
const raw = readFileSync(bookPath(id), "utf8");
|
const raw = readFileSync(bookPath(id), "utf8");
|
||||||
return JSON.parse(raw) as BookProject;
|
const book = JSON.parse(raw) as BookProject;
|
||||||
|
return {
|
||||||
|
...book,
|
||||||
|
creationMode: normalizeBookCreationMode(book.creationMode),
|
||||||
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBook(input: { title?: string }): BookProject {
|
export function createBook(input: {
|
||||||
|
title?: string;
|
||||||
|
creationMode?: BookCreationMode;
|
||||||
|
}): BookProject {
|
||||||
ensureUserDataDirs();
|
ensureUserDataDirs();
|
||||||
mkdirSync(booksDir(), { recursive: true });
|
mkdirSync(booksDir(), { recursive: true });
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const book: BookProject = {
|
const book: BookProject = {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
title: input.title?.trim() || "未命名作品",
|
title: input.title?.trim() || "未命名作品",
|
||||||
|
creationMode: normalizeBookCreationMode(input.creationMode),
|
||||||
preview: "新建作品,描述你想创作什么…",
|
preview: "新建作品,描述你想创作什么…",
|
||||||
sessionIds: [],
|
sessionIds: [],
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
@@ -100,6 +122,7 @@ export function updateBook(
|
|||||||
| "activeSessionId"
|
| "activeSessionId"
|
||||||
| "activeSkillId"
|
| "activeSkillId"
|
||||||
| "activeSkillName"
|
| "activeSkillName"
|
||||||
|
| "creationMode"
|
||||||
| "orchestratorId"
|
| "orchestratorId"
|
||||||
| "orchestratorName"
|
| "orchestratorName"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export type AgentMessageKind =
|
|||||||
| "orchestrator_prompt"
|
| "orchestrator_prompt"
|
||||||
| "orchestrator_assessment"
|
| "orchestrator_assessment"
|
||||||
| "dictate_reply"
|
| "dictate_reply"
|
||||||
|
| "listen_reply"
|
||||||
| "agent_tool"
|
| "agent_tool"
|
||||||
| "worker_running"
|
| "worker_running"
|
||||||
| "worker_output"
|
| "worker_output"
|
||||||
|
|||||||
+74
-24
@@ -1,5 +1,6 @@
|
|||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
import { listSkills } from "../skills/loader.js";
|
import { listSkills } from "../skills/loader.js";
|
||||||
|
import type { ModuleCatalogEntry } from "../skills/creation-flow.js";
|
||||||
import { createBook, deleteBook, duplicateBook, getBook, listBooks, updateBook } from "../book/store.js";
|
import { createBook, deleteBook, duplicateBook, getBook, listBooks, updateBook } from "../book/store.js";
|
||||||
import { sessionManager } from "./session-manager.js";
|
import { sessionManager } from "./session-manager.js";
|
||||||
|
|
||||||
@@ -39,9 +40,7 @@ function moduleStatusFromSections(
|
|||||||
return depth >= 2 ? "ready" : "partial";
|
return depth >= 2 ? "ready" : "partial";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadModulesPayload(skillId: string): Promise<{
|
type ModuleBrowseRow = {
|
||||||
skillPackId: string;
|
|
||||||
modules: Array<{
|
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
declaration: string;
|
declaration: string;
|
||||||
@@ -49,7 +48,56 @@ async function loadModulesPayload(skillId: string): Promise<{
|
|||||||
hasPrompt: boolean;
|
hasPrompt: boolean;
|
||||||
sectionsPresent: string[];
|
sectionsPresent: string[];
|
||||||
status: ModuleStatus;
|
status: ModuleStatus;
|
||||||
}>;
|
group: string;
|
||||||
|
presentMode: "message" | "zero_layer" | null;
|
||||||
|
layer: "intermediate" | "final" | null;
|
||||||
|
repeatable: boolean;
|
||||||
|
closer: boolean;
|
||||||
|
priorArtifact: boolean;
|
||||||
|
programSide: boolean;
|
||||||
|
libraries: string[];
|
||||||
|
land: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function moduleBrowseFields(
|
||||||
|
entry: ModuleCatalogEntry,
|
||||||
|
): Promise<
|
||||||
|
Omit<
|
||||||
|
ModuleBrowseRow,
|
||||||
|
| "id"
|
||||||
|
| "name"
|
||||||
|
| "declaration"
|
||||||
|
| "artifact"
|
||||||
|
| "hasPrompt"
|
||||||
|
| "sectionsPresent"
|
||||||
|
| "status"
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
const { dictateModuleLandHint, isDictateProgramModule } = await import(
|
||||||
|
"../skills/creation-flow.js"
|
||||||
|
);
|
||||||
|
const { moduleBrowseGroup, modulePresentationMode } = await import(
|
||||||
|
"../dictate/creation-plan.js"
|
||||||
|
);
|
||||||
|
const { getLibrary } = await import("../libraries/registry.js");
|
||||||
|
return {
|
||||||
|
group: moduleBrowseGroup(entry),
|
||||||
|
presentMode: modulePresentationMode(entry.id),
|
||||||
|
layer: entry.layer ?? null,
|
||||||
|
repeatable: Boolean(entry.repeatable),
|
||||||
|
closer: Boolean(entry.closer),
|
||||||
|
priorArtifact: entry.kind === "prior-artifact",
|
||||||
|
programSide: isDictateProgramModule(entry),
|
||||||
|
libraries: (entry.libraries ?? []).map(
|
||||||
|
(id) => getLibrary(id)?.label ?? id,
|
||||||
|
),
|
||||||
|
land: dictateModuleLandHint(entry),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadModulesPayload(skillId: string): Promise<{
|
||||||
|
groups: Array<{ id: string; label: string; hint: string }>;
|
||||||
|
modules: ModuleBrowseRow[];
|
||||||
}> {
|
}> {
|
||||||
const { loadSkill } = await import("../skills/loader.js");
|
const { loadSkill } = await import("../skills/loader.js");
|
||||||
const {
|
const {
|
||||||
@@ -59,13 +107,14 @@ async function loadModulesPayload(skillId: string): Promise<{
|
|||||||
catalogModulesForIntake,
|
catalogModulesForIntake,
|
||||||
MODULE_SECTION_IDS,
|
MODULE_SECTION_IDS,
|
||||||
} = await import("../skills/creation-flow.js");
|
} = await import("../skills/creation-flow.js");
|
||||||
|
const { MODULE_BROWSE_GROUPS } = await import("../dictate/creation-plan.js");
|
||||||
const skill = await loadSkill(skillId);
|
const skill = await loadSkill(skillId);
|
||||||
if (!skill.skillPackRoot) {
|
if (!skill.skillPackRoot) {
|
||||||
return { skillPackId: skillId, modules: [] };
|
return { groups: [...MODULE_BROWSE_GROUPS], modules: [] };
|
||||||
}
|
}
|
||||||
const catalog = await loadModuleCatalog(skill.skillPackRoot);
|
const catalog = await loadModuleCatalog(skill.skillPackRoot);
|
||||||
const modules = [];
|
const modules: ModuleBrowseRow[] = [];
|
||||||
for (const m of catalogModulesForIntake(catalog ?? { modules: [] }, "recipe")) {
|
for (const m of catalogModulesForIntake(catalog ?? { modules: [] }, "dictate")) {
|
||||||
const prompt = await loadModulePrompt(skill.skillPackRoot, m.id);
|
const prompt = await loadModulePrompt(skill.skillPackRoot, m.id);
|
||||||
const sections = prompt
|
const sections = prompt
|
||||||
? parseModulePromptSections(prompt)
|
? parseModulePromptSections(prompt)
|
||||||
@@ -83,27 +132,23 @@ async function loadModulesPayload(skillId: string): Promise<{
|
|||||||
hasPrompt,
|
hasPrompt,
|
||||||
sectionsPresent: [...sectionsPresent],
|
sectionsPresent: [...sectionsPresent],
|
||||||
status: moduleStatusFromSections(hasPrompt, sectionsPresent, taskBody),
|
status: moduleStatusFromSections(hasPrompt, sectionsPresent, taskBody),
|
||||||
|
...(await moduleBrowseFields(m)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { skillPackId: skillId, modules };
|
return { groups: [...MODULE_BROWSE_GROUPS], modules };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadModuleDetailPayload(
|
async function loadModuleDetailPayload(
|
||||||
skillId: string,
|
skillId: string,
|
||||||
moduleId: string,
|
moduleId: string,
|
||||||
): Promise<{
|
): Promise<
|
||||||
skillPackId: string;
|
| (ModuleBrowseRow & {
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
declaration: string;
|
|
||||||
artifact: string;
|
|
||||||
hasPrompt: boolean;
|
|
||||||
sectionsPresent: string[];
|
|
||||||
status: ModuleStatus;
|
|
||||||
sections: Record<string, string>;
|
sections: Record<string, string>;
|
||||||
meta: Record<string, unknown> | null;
|
meta: Record<string, unknown> | null;
|
||||||
raw: string;
|
raw: string;
|
||||||
} | null> {
|
})
|
||||||
|
| null
|
||||||
|
> {
|
||||||
const { loadSkill } = await import("../skills/loader.js");
|
const { loadSkill } = await import("../skills/loader.js");
|
||||||
const {
|
const {
|
||||||
loadModuleCatalog,
|
loadModuleCatalog,
|
||||||
@@ -141,7 +186,6 @@ async function loadModuleDetailPayload(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
skillPackId: skillId,
|
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
declaration: entry.declaration,
|
declaration: entry.declaration,
|
||||||
@@ -153,6 +197,7 @@ async function loadModuleDetailPayload(
|
|||||||
sectionsPresent,
|
sectionsPresent,
|
||||||
sections.task,
|
sections.task,
|
||||||
),
|
),
|
||||||
|
...(await moduleBrowseFields(entry)),
|
||||||
sections,
|
sections,
|
||||||
meta,
|
meta,
|
||||||
raw: prompt ?? "",
|
raw: prompt ?? "",
|
||||||
@@ -254,7 +299,7 @@ export async function handleBooksApi(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 默认配方包的技能池(编排备选) */
|
/** 对话落盘节点:按角色分组,不含旧配方包 */
|
||||||
if (pathname === "/api/modules" && req.method === "GET") {
|
if (pathname === "/api/modules" && req.method === "GET") {
|
||||||
const { DEFAULT_ORCHESTRATOR_ID } = await import(
|
const { DEFAULT_ORCHESTRATOR_ID } = await import(
|
||||||
"../config/default-orchestrator.js"
|
"../config/default-orchestrator.js"
|
||||||
@@ -343,8 +388,8 @@ export async function handleBooksApi(
|
|||||||
orchestratorId?: string;
|
orchestratorId?: string;
|
||||||
/** 旧客户端可传;新建作品改由创作对话判断路径 */
|
/** 旧客户端可传;新建作品改由创作对话判断路径 */
|
||||||
recipeId?: string;
|
recipeId?: string;
|
||||||
/** 旧客户端兼容;新建作品统一使用 dictate */
|
/** 进料方式:对话落盘(默认)| 听书连载 | 旧工序编排 */
|
||||||
creationMode?: "recipe" | "dictate";
|
creationMode?: "recipe" | "dictate" | "listen_serial";
|
||||||
};
|
};
|
||||||
const skills = await listSkills();
|
const skills = await listSkills();
|
||||||
const requested = body.orchestratorId?.trim();
|
const requested = body.orchestratorId?.trim();
|
||||||
@@ -356,14 +401,19 @@ export async function handleBooksApi(
|
|||||||
json(res, 400, { error: "没有可用的编排器技能包" });
|
json(res, 400, { error: "没有可用的编排器技能包" });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const book = createBook({ title: body.title });
|
const rawMode = body.creationMode;
|
||||||
|
const creationMode =
|
||||||
|
rawMode === "listen_serial" || rawMode === "recipe" || rawMode === "dictate"
|
||||||
|
? rawMode
|
||||||
|
: "dictate";
|
||||||
|
const book = createBook({ title: body.title, creationMode });
|
||||||
updateBook(book.id, {
|
updateBook(book.id, {
|
||||||
activeSkillId: director.name,
|
activeSkillId: director.name,
|
||||||
activeSkillName: director.description?.split("\n")[0]?.slice(0, 80) || director.name,
|
activeSkillName: director.description?.split("\n")[0]?.slice(0, 80) || director.name,
|
||||||
orchestratorId: director.name,
|
orchestratorId: director.name,
|
||||||
orchestratorName: director.name,
|
orchestratorName: director.name,
|
||||||
|
creationMode,
|
||||||
});
|
});
|
||||||
const creationMode = "dictate";
|
|
||||||
const session = await sessionManager.createForBook(
|
const session = await sessionManager.createForBook(
|
||||||
book.id,
|
book.id,
|
||||||
director.name,
|
director.name,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const PHASE_UNIT_LABELS: Record<string, string> = {
|
|||||||
const CREATION_MODE_LABELS: Record<string, string> = {
|
const CREATION_MODE_LABELS: Record<string, string> = {
|
||||||
recipe: "工序编排",
|
recipe: "工序编排",
|
||||||
dictate: "对话落盘",
|
dictate: "对话落盘",
|
||||||
|
listen_serial: "听书连载",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SKILL_PACK_LABELS: Record<string, string> = {
|
const SKILL_PACK_LABELS: Record<string, string> = {
|
||||||
|
|||||||
+295
-31
@@ -93,6 +93,7 @@ import {
|
|||||||
sessionSkillPackId,
|
sessionSkillPackId,
|
||||||
skillPacksMatch,
|
skillPacksMatch,
|
||||||
} from "../book/skill-id.js";
|
} from "../book/skill-id.js";
|
||||||
|
import type { RequestOutline } from "../llm/context-outline.js";
|
||||||
import {
|
import {
|
||||||
pruneContextTraces,
|
pruneContextTraces,
|
||||||
type LlmContextTrace,
|
type LlmContextTrace,
|
||||||
@@ -102,6 +103,8 @@ import type { BlackboardItem } from "../types/blackboard.js";
|
|||||||
import { Blackboard } from "../blackboard/blackboard.js";
|
import { Blackboard } from "../blackboard/blackboard.js";
|
||||||
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
|
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
|
||||||
import { parseShellAdaptationFromReplyFormat } from "../skills/present-packet.js";
|
import { parseShellAdaptationFromReplyFormat } from "../skills/present-packet.js";
|
||||||
|
import { parseReplyDataContract } from "../skills/reply-contract.js";
|
||||||
|
import { projectPresentBlocks } from "../skills/present-project.js";
|
||||||
import {
|
import {
|
||||||
CREATION_ACCEPTED_CONTENT_TAG,
|
CREATION_ACCEPTED_CONTENT_TAG,
|
||||||
CREATION_ACCEPTED_UNITS_TAG,
|
CREATION_ACCEPTED_UNITS_TAG,
|
||||||
@@ -182,6 +185,16 @@ import {
|
|||||||
sortDictateProducts,
|
sortDictateProducts,
|
||||||
type DictateProduct,
|
type DictateProduct,
|
||||||
} from "../dictate/index.js";
|
} from "../dictate/index.js";
|
||||||
|
import {
|
||||||
|
LISTEN_SERIAL_MODE_VALUE,
|
||||||
|
isListenSerialModeValue,
|
||||||
|
markListenConsumed,
|
||||||
|
readListenState,
|
||||||
|
runListenSerialTurn,
|
||||||
|
toListenView,
|
||||||
|
type ListenSerialView,
|
||||||
|
} from "../listen-serial/index.js";
|
||||||
|
import { synthesizeSpeech, type TtsSynthesizeResult } from "../tts/index.js";
|
||||||
import {
|
import {
|
||||||
formatQuestionAnswersForAi,
|
formatQuestionAnswersForAi,
|
||||||
formatQuestionAnswersForDisplay,
|
formatQuestionAnswersForDisplay,
|
||||||
@@ -255,6 +268,8 @@ export type ChatMessage = {
|
|||||||
thinking?: string;
|
thinking?: string;
|
||||||
/** 该次 LLM 请求实际携带的全量上下文(观察用;按设置只保留最新 N 条) */
|
/** 该次 LLM 请求实际携带的全量上下文(观察用;按设置只保留最新 N 条) */
|
||||||
contextTrace?: LlmContextTrace;
|
contextTrace?: LlmContextTrace;
|
||||||
|
/** 本条回复对应的各次请求标签轮廓(选题、填写都在;不含正文) */
|
||||||
|
requestOutlines?: RequestOutline[];
|
||||||
/** run 验收后过程讨论折叠(主 feed 隐藏);创作 messages 不删不藏 */
|
/** run 验收后过程讨论折叠(主 feed 隐藏);创作 messages 不删不藏 */
|
||||||
compressed?: boolean;
|
compressed?: boolean;
|
||||||
tokenUsage?: MessageTokenUsage;
|
tokenUsage?: MessageTokenUsage;
|
||||||
@@ -323,6 +338,14 @@ export type SessionView = {
|
|||||||
block_labels?: Record<string, string>;
|
block_labels?: Record<string, string>;
|
||||||
empty_states?: Record<string, string>;
|
empty_states?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
/** 正文组成固定前端。游玩和开场把 present.v1 灌进去,替换示例内容。 */
|
||||||
|
presentationFrontend?: { html: string; css: string; js: string };
|
||||||
|
/** 正文组成数据契约里的区。格式化时按 id 填进对应 data-block。 */
|
||||||
|
presentationSlots?: Array<{ id: string; type: string }>;
|
||||||
|
/**
|
||||||
|
* 程序投影块(来自 变量.当前 等)。前端灌 present 时与消息正文合并,随真值刷新。
|
||||||
|
*/
|
||||||
|
presentationProjection?: Record<string, unknown>;
|
||||||
/** 总管正在输出的思维链(流式,轮询用) */
|
/** 总管正在输出的思维链(流式,轮询用) */
|
||||||
agentThinking?: string;
|
agentThinking?: string;
|
||||||
/** Agent / Worker 流式输出(轮询用) */
|
/** Agent / Worker 流式输出(轮询用) */
|
||||||
@@ -333,10 +356,12 @@ export type SessionView = {
|
|||||||
*/
|
*/
|
||||||
runActive?: boolean;
|
runActive?: boolean;
|
||||||
resumed?: boolean;
|
resumed?: boolean;
|
||||||
/** 创作进料:recipe=配方节点流;dictate=转述整理直填 */
|
/** 创作进料:recipe=配方节点流;dictate=转述整理;listen_serial=听书连载 */
|
||||||
creationMode?: "recipe" | "dictate";
|
creationMode?: "recipe" | "dictate" | "listen_serial";
|
||||||
/** 转述式已写入黑板的产物(按相对序) */
|
/** 转述式已写入黑板的产物(按相对序) */
|
||||||
dictateProducts?: Array<{ tag: string; content: string; order?: number }>;
|
dictateProducts?: Array<{ tag: string; content: string; order?: number }>;
|
||||||
|
/** 听书连载投影(大纲/正文缓冲/未读字数) */
|
||||||
|
listenSerial?: ListenSerialView;
|
||||||
tokenStats?: {
|
tokenStats?: {
|
||||||
sessionTotal: number;
|
sessionTotal: number;
|
||||||
sessionCached?: number;
|
sessionCached?: number;
|
||||||
@@ -555,8 +580,16 @@ export class SessionManager {
|
|||||||
this.writeOpeningPersonaChoice(s, true);
|
this.writeOpeningPersonaChoice(s, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 与进游玩一致:用完整文档(含 present.v1),勿用侧栏预览的压扁正文
|
||||||
|
const rawOnBoard = board.getContentByTag(picked.tag)?.trim() ?? "";
|
||||||
|
const body =
|
||||||
|
splitOpeningDocument(
|
||||||
|
rawOnBoard || picked.document || picked.text,
|
||||||
|
).text.trim() ||
|
||||||
|
picked.document?.trim() ||
|
||||||
|
picked.text.trim();
|
||||||
const expanded = this.expandDirectivesText(
|
const expanded = this.expandDirectivesText(
|
||||||
picked.text,
|
body,
|
||||||
this.playDirectiveContext(s),
|
this.playDirectiveContext(s),
|
||||||
);
|
);
|
||||||
board.write({
|
board.write({
|
||||||
@@ -566,7 +599,13 @@ export class SessionManager {
|
|||||||
});
|
});
|
||||||
const openingAt = s.messages.findIndex((m) => m.title === "开场白");
|
const openingAt = s.messages.findIndex((m) => m.title === "开场白");
|
||||||
if (openingAt >= 0) {
|
if (openingAt >= 0) {
|
||||||
s.messages[openingAt] = this.playOpeningMessage(expanded);
|
// 保留 id,前端才能 patch 呈现壳 forceFill,而不是拆 iframe 后软灌样本槽
|
||||||
|
s.messages[openingAt] = this.playOpeningMessage(
|
||||||
|
expanded,
|
||||||
|
s.messages[openingAt]!.id,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
s.messages.unshift(this.playOpeningMessage(expanded));
|
||||||
}
|
}
|
||||||
if (s.bookId) this.persist(s);
|
if (s.bookId) this.persist(s);
|
||||||
return this.toView(sessionId);
|
return this.toView(sessionId);
|
||||||
@@ -613,11 +652,13 @@ export class SessionManager {
|
|||||||
|
|
||||||
const next = applyContextOrderEdit(current!, edit);
|
const next = applyContextOrderEdit(current!, edit);
|
||||||
const serialized = serializeContextOrder(next);
|
const serialized = serializeContextOrder(next);
|
||||||
|
const orderMeta = bb.getLatestByTag(CONTEXT_ORDER_TAG)?.metadata;
|
||||||
|
|
||||||
bb.write({
|
bb.write({
|
||||||
tag: CONTEXT_ORDER_TAG,
|
tag: CONTEXT_ORDER_TAG,
|
||||||
content: serialized,
|
content: serialized,
|
||||||
source: "user",
|
source: "user",
|
||||||
|
metadata: orderMeta,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (workerSetRaw) {
|
if (workerSetRaw) {
|
||||||
@@ -631,6 +672,7 @@ export class SessionManager {
|
|||||||
tag: targetTag,
|
tag: targetTag,
|
||||||
content: mergeContextOrderIntoWorkerSetJson(workerSetRaw, next),
|
content: mergeContextOrderIntoWorkerSetJson(workerSetRaw, next),
|
||||||
source: "user",
|
source: "user",
|
||||||
|
metadata: bb.getLatestByTag(targetTag)?.metadata,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -639,6 +681,7 @@ export class SessionManager {
|
|||||||
tag: CONTEXT_ORDER_TAG,
|
tag: CONTEXT_ORDER_TAG,
|
||||||
content: serialized,
|
content: serialized,
|
||||||
source: "user",
|
source: "user",
|
||||||
|
metadata: orderMeta,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,7 +713,7 @@ export class SessionManager {
|
|||||||
bookId?: string,
|
bookId?: string,
|
||||||
preselectSkillId?: string,
|
preselectSkillId?: string,
|
||||||
selectedRecipeId?: string,
|
selectedRecipeId?: string,
|
||||||
opts?: { creationMode?: "recipe" | "dictate" },
|
opts?: { creationMode?: "recipe" | "dictate" | "listen_serial" },
|
||||||
): Promise<SessionView> {
|
): Promise<SessionView> {
|
||||||
const id = randomUUID();
|
const id = randomUUID();
|
||||||
const messages: ChatMessage[] = [];
|
const messages: ChatMessage[] = [];
|
||||||
@@ -713,18 +756,22 @@ export class SessionManager {
|
|||||||
} else {
|
} else {
|
||||||
await runtime.start();
|
await runtime.start();
|
||||||
}
|
}
|
||||||
const useDictate = opts?.creationMode === "dictate";
|
const creationMode = opts?.creationMode ?? "recipe";
|
||||||
|
const useDictate = creationMode === "dictate";
|
||||||
|
const useListen = creationMode === "listen_serial";
|
||||||
await this.refreshRecipeOptions(
|
await this.refreshRecipeOptions(
|
||||||
managed,
|
managed,
|
||||||
useDictate ? "dictate" : "recipe",
|
useDictate || useListen ? "dictate" : "recipe",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedRecipeId?.trim()) {
|
if (selectedRecipeId?.trim()) {
|
||||||
await this.applySelectedRecipe(managed, selectedRecipeId.trim(), {
|
await this.applySelectedRecipe(managed, selectedRecipeId.trim(), {
|
||||||
seedFlow: !useDictate,
|
seedFlow: !useDictate && !useListen,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (useDictate) {
|
if (useListen) {
|
||||||
|
this.enterListenSerialMode(managed);
|
||||||
|
} else if (useDictate) {
|
||||||
this.enterDictateMode(managed);
|
this.enterDictateMode(managed);
|
||||||
} else {
|
} else {
|
||||||
await this.maybeKickAgentAfterDemand(runtime);
|
await this.maybeKickAgentAfterDemand(runtime);
|
||||||
@@ -780,6 +827,145 @@ export class SessionManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 听书连载:薄上下文连续产文,不走对话落盘工具环 */
|
||||||
|
private enterListenSerialMode(s: ManagedSession): void {
|
||||||
|
const board = s.runtime.getBlackboard();
|
||||||
|
board.write({
|
||||||
|
tag: CREATION_INTAKE_MODE_TAG,
|
||||||
|
content: LISTEN_SERIAL_MODE_VALUE,
|
||||||
|
source: "runtime",
|
||||||
|
});
|
||||||
|
s.runtime.enterDictateIntake(
|
||||||
|
"听书连载:用几句话描述题材与爽点;确认后我会生成大纲并自动补正文。不满意就说一句意见改后面。",
|
||||||
|
);
|
||||||
|
const welcome =
|
||||||
|
"听书连载:说几句你想听的故事(题材、人设、爽点即可)。我会轻回问或给出草案;确认后自动出大纲和正文缓冲。听的过程中随时一句意见,只改后面的走向。";
|
||||||
|
s.messages.push({
|
||||||
|
id: randomUUID(),
|
||||||
|
role: "system",
|
||||||
|
text: welcome,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
kind: "orchestrator_prompt",
|
||||||
|
actor: "orchestrator",
|
||||||
|
title: "听书连载",
|
||||||
|
body: welcome,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private isListenSerialMode(s: ManagedSession): boolean {
|
||||||
|
return isListenSerialModeValue(
|
||||||
|
s.runtime.getBlackboard().getContentByTag(CREATION_INTAKE_MODE_TAG),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private creationIntakeMode(
|
||||||
|
s: ManagedSession,
|
||||||
|
): "recipe" | "dictate" | "listen_serial" {
|
||||||
|
if (this.isListenSerialMode(s)) return "listen_serial";
|
||||||
|
if (this.isDictateMode(s)) return "dictate";
|
||||||
|
return "recipe";
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractListenDialogue(
|
||||||
|
messages: ChatMessage[],
|
||||||
|
): Array<{ role: "user" | "assistant"; text: string }> {
|
||||||
|
const out: Array<{ role: "user" | "assistant"; text: string }> = [];
|
||||||
|
for (const m of messages) {
|
||||||
|
if (m.kind === "user_input" || m.role === "user") {
|
||||||
|
const t = (m.body ?? m.text ?? "").trim();
|
||||||
|
if (t) out.push({ role: "user", text: t });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (m.kind === "listen_reply") {
|
||||||
|
const t = (m.body ?? m.text ?? "").trim();
|
||||||
|
if (t) out.push({ role: "assistant", text: t });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runListenSerialTurnForSession(
|
||||||
|
s: ManagedSession,
|
||||||
|
userText: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const llm = createDefaultMainAgentLlm(s.trackingRef, () =>
|
||||||
|
this.personaForManaged(s),
|
||||||
|
);
|
||||||
|
const dialogue = this.extractListenDialogue(s.messages);
|
||||||
|
const result = await runListenSerialTurn({
|
||||||
|
llm,
|
||||||
|
board: s.runtime.getBlackboard(),
|
||||||
|
dialogue,
|
||||||
|
userText,
|
||||||
|
});
|
||||||
|
const keep = normalizeContextTraceKeepLatest(
|
||||||
|
loadAppSettings().contextTraceKeepLatest,
|
||||||
|
);
|
||||||
|
const requestOutlines = s.trackingRef.current.pendingOutlines;
|
||||||
|
const pendingTrace = s.trackingRef.current.pendingContextTrace;
|
||||||
|
const tokenUsage = s.trackingRef.current.pendingUsage;
|
||||||
|
s.trackingRef.current.pendingOutlines = undefined;
|
||||||
|
s.trackingRef.current.pendingContextTrace = undefined;
|
||||||
|
s.trackingRef.current.pendingUsage = undefined;
|
||||||
|
s.messages.push({
|
||||||
|
id: randomUUID(),
|
||||||
|
role: "system",
|
||||||
|
text: result.reply,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
kind: "listen_reply",
|
||||||
|
actor: "orchestrator",
|
||||||
|
title: "听书连载",
|
||||||
|
body: result.reply,
|
||||||
|
...(tokenUsage ? { tokenUsage } : {}),
|
||||||
|
...(keep > 0 && pendingTrace ? { contextTrace: pendingTrace } : {}),
|
||||||
|
...(requestOutlines?.length ? { requestOutlines } : {}),
|
||||||
|
});
|
||||||
|
s.messages = pruneContextTraces(s.messages, keep);
|
||||||
|
s.runtime.enterDictateIntake(
|
||||||
|
result.softStop
|
||||||
|
? "本段已告一段落。输入「下一波」或「收工」,或一句意见改向。"
|
||||||
|
: "继续听/读,或说「继续」补货;不满意直接一句意见。",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 听书:标记已消费字数(阅读进度) */
|
||||||
|
markListenSerialConsumed(
|
||||||
|
sessionId: string,
|
||||||
|
consumedChars: number,
|
||||||
|
): SessionView {
|
||||||
|
const s = this.require(sessionId);
|
||||||
|
if (!this.isListenSerialMode(s)) {
|
||||||
|
throw new Error("当前不是听书连载模式");
|
||||||
|
}
|
||||||
|
markListenConsumed(s.runtime.getBlackboard(), consumedChars);
|
||||||
|
if (s.bookId) this.persist(s);
|
||||||
|
return this.toView(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 听书:按节合成语音(默认 Edge) */
|
||||||
|
async synthesizeListenSection(
|
||||||
|
sessionId: string,
|
||||||
|
sectionId: string,
|
||||||
|
): Promise<TtsSynthesizeResult & { sectionId: string; title: string }> {
|
||||||
|
const s = this.require(sessionId);
|
||||||
|
if (!this.isListenSerialMode(s)) {
|
||||||
|
throw new Error("当前不是听书连载模式");
|
||||||
|
}
|
||||||
|
const id = sectionId.trim();
|
||||||
|
if (!id) throw new Error("需要 sectionId");
|
||||||
|
const state = readListenState(s.runtime.getBlackboard());
|
||||||
|
const section = state.sections.find((x) => x.id === id);
|
||||||
|
if (!section?.text?.trim()) {
|
||||||
|
throw new Error("未找到该节正文");
|
||||||
|
}
|
||||||
|
const spoken = `【${section.title}】\n${section.text}`.trim();
|
||||||
|
const result = await synthesizeSpeech({
|
||||||
|
text: spoken,
|
||||||
|
cacheKey: `${sessionId}:${id}`,
|
||||||
|
});
|
||||||
|
return { ...result, sectionId: id, title: section.title };
|
||||||
|
}
|
||||||
|
|
||||||
private listDictateProducts(s: ManagedSession): DictateProduct[] {
|
private listDictateProducts(s: ManagedSession): DictateProduct[] {
|
||||||
const board = s.runtime.getBlackboard();
|
const board = s.runtime.getBlackboard();
|
||||||
const products: DictateProduct[] = [];
|
const products: DictateProduct[] = [];
|
||||||
@@ -897,11 +1083,10 @@ export class SessionManager {
|
|||||||
if (dictateProductFamily(tag) === OPENING_PRODUCT_FAMILY) {
|
if (dictateProductFamily(tag) === OPENING_PRODUCT_FAMILY) {
|
||||||
const split = splitOpeningDocument(content);
|
const split = splitOpeningDocument(content);
|
||||||
if (split.persona) {
|
if (split.persona) {
|
||||||
ensureProtagonistFromPersona(
|
const personaTag = protagonistTagForOpeningSlot(tag);
|
||||||
board,
|
ensureProtagonistFromPersona(board, split.persona, personaTag, {
|
||||||
split.persona,
|
overwrite: personaTag !== PROTAGONIST_TAG,
|
||||||
protagonistTagForOpeningSlot(tag),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// 仅有用户角色 meta、无叙事:抬到主角设定,不污染开场白
|
// 仅有用户角色 meta、无叙事:抬到主角设定,不污染开场白
|
||||||
if (split.persona && !split.text.trim()) {
|
if (split.persona && !split.text.trim()) {
|
||||||
@@ -1009,6 +1194,15 @@ export class SessionManager {
|
|||||||
s.messages.length,
|
s.messages.length,
|
||||||
this.captureCheckpoint(s),
|
this.captureCheckpoint(s),
|
||||||
);
|
);
|
||||||
|
const keep = normalizeContextTraceKeepLatest(
|
||||||
|
loadAppSettings().contextTraceKeepLatest,
|
||||||
|
);
|
||||||
|
const requestOutlines = s.trackingRef.current.pendingOutlines;
|
||||||
|
const pendingTrace = s.trackingRef.current.pendingContextTrace;
|
||||||
|
const tokenUsage = s.trackingRef.current.pendingUsage;
|
||||||
|
s.trackingRef.current.pendingOutlines = undefined;
|
||||||
|
s.trackingRef.current.pendingContextTrace = undefined;
|
||||||
|
s.trackingRef.current.pendingUsage = undefined;
|
||||||
s.messages.push({
|
s.messages.push({
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -1019,7 +1213,11 @@ export class SessionManager {
|
|||||||
title: "转述整理",
|
title: "转述整理",
|
||||||
body: result.reply,
|
body: result.reply,
|
||||||
...(thinking ? { thinking } : {}),
|
...(thinking ? { thinking } : {}),
|
||||||
|
...(tokenUsage ? { tokenUsage } : {}),
|
||||||
|
...(keep > 0 && pendingTrace ? { contextTrace: pendingTrace } : {}),
|
||||||
|
...(requestOutlines?.length ? { requestOutlines } : {}),
|
||||||
});
|
});
|
||||||
|
s.messages = pruneContextTraces(s.messages, keep);
|
||||||
s.runtime.enterDictateIntake();
|
s.runtime.enterDictateIntake();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1494,6 +1692,7 @@ export class SessionManager {
|
|||||||
// 转述聊天常夹确认/建议,不能当开场白
|
// 转述聊天常夹确认/建议,不能当开场白
|
||||||
if (
|
if (
|
||||||
kind === "dictate_reply" ||
|
kind === "dictate_reply" ||
|
||||||
|
kind === "listen_reply" ||
|
||||||
kind === "agent_tool" ||
|
kind === "agent_tool" ||
|
||||||
kind === "orchestrator_thinking" ||
|
kind === "orchestrator_thinking" ||
|
||||||
kind === "worker_questions"
|
kind === "worker_questions"
|
||||||
@@ -1830,9 +2029,9 @@ export class SessionManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private playOpeningMessage(text: string): ChatMessage {
|
private playOpeningMessage(text: string, keepId?: string): ChatMessage {
|
||||||
return {
|
return {
|
||||||
id: randomUUID(),
|
id: keepId || randomUUID(),
|
||||||
role: "system",
|
role: "system",
|
||||||
text,
|
text,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -1917,6 +2116,29 @@ export class SessionManager {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换/改了全局用户角色:游玩层开场按新 @玩家 重展开,并改用全局角色卡。
|
||||||
|
*/
|
||||||
|
rebindPlayOpeningsForActivePersona(): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const [sessionId, s] of this.sessions) {
|
||||||
|
if (!isPlayLayerActive(s.runtime.getSession().slots)) continue;
|
||||||
|
if (inferLifecycleStage(s.runtime.getSession()) !== "play") continue;
|
||||||
|
const aux = buildPlayAuxView(
|
||||||
|
s.runtime.getBlackboard(),
|
||||||
|
s.runtime.getSession().slots,
|
||||||
|
);
|
||||||
|
if (!aux.openings.length) continue;
|
||||||
|
try {
|
||||||
|
this.selectPlayOpening(sessionId, aux.selectedOpeningIndex, false);
|
||||||
|
count += 1;
|
||||||
|
} catch {
|
||||||
|
/* 单局失败不挡其它局 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
async sendMessage(
|
async sendMessage(
|
||||||
id: string,
|
id: string,
|
||||||
text: string,
|
text: string,
|
||||||
@@ -1983,15 +2205,17 @@ export class SessionManager {
|
|||||||
const inPlay =
|
const inPlay =
|
||||||
isPlayLayerActive(sess.slots) ||
|
isPlayLayerActive(sess.slots) ||
|
||||||
inferLifecycleStage(sess) === "play";
|
inferLifecycleStage(sess) === "play";
|
||||||
if (
|
if (reason?.kind === "input" && !inPlay) {
|
||||||
this.isDictateMode(s) &&
|
if (this.isListenSerialMode(s)) {
|
||||||
reason?.kind === "input" &&
|
await this.runListenSerialTurnForSession(s, trimmed || text);
|
||||||
!inPlay
|
return;
|
||||||
) {
|
}
|
||||||
|
if (this.isDictateMode(s)) {
|
||||||
await this.runDictateTurnForSession(s, trimmed || text);
|
await this.runDictateTurnForSession(s, trimmed || text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (reason?.kind === "approve_step") {
|
if (reason?.kind === "approve_step") {
|
||||||
await s.runtime.rejectStep(trimmed || text);
|
await s.runtime.rejectStep(trimmed || text);
|
||||||
} else if (reason?.kind === "review_artifact") {
|
} else if (reason?.kind === "review_artifact") {
|
||||||
@@ -2237,14 +2461,16 @@ export class SessionManager {
|
|||||||
const inPlay =
|
const inPlay =
|
||||||
isPlayLayerActive(sess.slots) ||
|
isPlayLayerActive(sess.slots) ||
|
||||||
inferLifecycleStage(sess) === "play";
|
inferLifecycleStage(sess) === "play";
|
||||||
if (
|
if (reason?.kind === "input" && !inPlay) {
|
||||||
this.isDictateMode(s) &&
|
if (this.isListenSerialMode(s)) {
|
||||||
reason?.kind === "input" &&
|
await this.runListenSerialTurnForSession(s, trimmed);
|
||||||
!inPlay
|
return;
|
||||||
) {
|
}
|
||||||
|
if (this.isDictateMode(s)) {
|
||||||
await this.runDictateTurnForSession(s, trimmed);
|
await this.runDictateTurnForSession(s, trimmed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
await s.runtime.submitInput(trimmed);
|
await s.runtime.submitInput(trimmed);
|
||||||
});
|
});
|
||||||
if (!owned) return this.toView(id);
|
if (!owned) return this.toView(id);
|
||||||
@@ -3100,6 +3326,7 @@ export class SessionManager {
|
|||||||
undefined;
|
undefined;
|
||||||
const pendingTrace = managed.trackingRef.current.pendingContextTrace;
|
const pendingTrace = managed.trackingRef.current.pendingContextTrace;
|
||||||
let contextTrace: LlmContextTrace | undefined;
|
let contextTrace: LlmContextTrace | undefined;
|
||||||
|
let requestOutlines: RequestOutline[] | undefined;
|
||||||
if (
|
if (
|
||||||
pendingTrace &&
|
pendingTrace &&
|
||||||
classified &&
|
classified &&
|
||||||
@@ -3112,10 +3339,19 @@ export class SessionManager {
|
|||||||
contextTrace = pendingTrace;
|
contextTrace = pendingTrace;
|
||||||
}
|
}
|
||||||
managed.trackingRef.current.pendingContextTrace = undefined;
|
managed.trackingRef.current.pendingContextTrace = undefined;
|
||||||
|
requestOutlines = managed.trackingRef.current.pendingOutlines;
|
||||||
|
managed.trackingRef.current.pendingOutlines = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
managed.messages.push(
|
managed.messages.push(
|
||||||
this.msg("system", text, usage, thinking, contextTrace),
|
this.msg(
|
||||||
|
"system",
|
||||||
|
text,
|
||||||
|
usage,
|
||||||
|
thinking,
|
||||||
|
contextTrace,
|
||||||
|
requestOutlines?.length ? requestOutlines : undefined,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
managed.messages = pruneContextTraces(
|
managed.messages = pruneContextTraces(
|
||||||
managed.messages,
|
managed.messages,
|
||||||
@@ -3405,6 +3641,7 @@ export class SessionManager {
|
|||||||
tokenUsage?: MessageTokenUsage,
|
tokenUsage?: MessageTokenUsage,
|
||||||
thinking?: string,
|
thinking?: string,
|
||||||
contextTrace?: LlmContextTrace,
|
contextTrace?: LlmContextTrace,
|
||||||
|
requestOutlines?: RequestOutline[],
|
||||||
): ChatMessage {
|
): ChatMessage {
|
||||||
const base = {
|
const base = {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
@@ -3414,6 +3651,7 @@ export class SessionManager {
|
|||||||
...(tokenUsage ? { tokenUsage } : {}),
|
...(tokenUsage ? { tokenUsage } : {}),
|
||||||
...(thinking ? { thinking } : {}),
|
...(thinking ? { thinking } : {}),
|
||||||
...(contextTrace ? { contextTrace } : {}),
|
...(contextTrace ? { contextTrace } : {}),
|
||||||
|
...(requestOutlines?.length ? { requestOutlines } : {}),
|
||||||
};
|
};
|
||||||
if (role === "user") {
|
if (role === "user") {
|
||||||
return {
|
return {
|
||||||
@@ -3812,10 +4050,12 @@ export class SessionManager {
|
|||||||
lifecycleStage,
|
lifecycleStage,
|
||||||
{ workerSetYaml },
|
{ workerSetYaml },
|
||||||
);
|
);
|
||||||
const shellAdapt = parseShellAdaptationFromReplyFormat(
|
const replyFormatRaw =
|
||||||
s.runtime.getBlackboard().getContentByTag("设计.正文组成") ??
|
s.runtime.getBlackboard().getContentByTag("设计.正文组成") ??
|
||||||
s.runtime.getBlackboard().getContentByTag("设计.回复格式"),
|
s.runtime.getBlackboard().getContentByTag("设计.0层正文组成") ??
|
||||||
);
|
s.runtime.getBlackboard().getContentByTag("设计.回复格式");
|
||||||
|
const shellAdapt = parseShellAdaptationFromReplyFormat(replyFormatRaw);
|
||||||
|
const replyContract = parseReplyDataContract(replyFormatRaw);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -3831,10 +4071,13 @@ export class SessionManager {
|
|||||||
actions,
|
actions,
|
||||||
pipeline: buildPipeline(session),
|
pipeline: buildPipeline(session),
|
||||||
lifecycleStage,
|
lifecycleStage,
|
||||||
creationMode: this.isDictateMode(s) ? "dictate" : "recipe",
|
creationMode: this.creationIntakeMode(s),
|
||||||
dictateProducts: this.isDictateMode(s)
|
dictateProducts: this.isDictateMode(s)
|
||||||
? this.listDictateProducts(s)
|
? this.listDictateProducts(s)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
listenSerial: this.isListenSerialMode(s)
|
||||||
|
? toListenView(readListenState(s.runtime.getBlackboard()))
|
||||||
|
: undefined,
|
||||||
playLayerActive: isPlayLayerActive(session.slots),
|
playLayerActive: isPlayLayerActive(session.slots),
|
||||||
playReady: canEnterPlay(session),
|
playReady: canEnterPlay(session),
|
||||||
hasProduct: Boolean(
|
hasProduct: Boolean(
|
||||||
@@ -3895,6 +4138,27 @@ export class SessionManager {
|
|||||||
...shellAdapt.tweaks,
|
...shellAdapt.tweaks,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
presentationFrontend: replyContract?.frontend,
|
||||||
|
presentationSlots: replyContract
|
||||||
|
? [...replyContract.modelBlocks, ...replyContract.programBlocks].map((block) => ({
|
||||||
|
id: block.id,
|
||||||
|
type: block.type,
|
||||||
|
}))
|
||||||
|
: undefined,
|
||||||
|
presentationProjection: (() => {
|
||||||
|
if (!replyContract?.programBlocks.length) return undefined;
|
||||||
|
const board = s.runtime.getBlackboard();
|
||||||
|
const projected = projectPresentBlocks({
|
||||||
|
contract: replyContract,
|
||||||
|
statusBarRaw:
|
||||||
|
board.getContentByTag("设计.监控栏") ??
|
||||||
|
board.getContentByTag("设计.回复呈现"),
|
||||||
|
currentVarsRaw: board.getContentByTag("变量.当前"),
|
||||||
|
topologyRaw: board.getContentByTag("运行.拓扑"),
|
||||||
|
readTag: (tag) => board.getContentByTag(tag),
|
||||||
|
});
|
||||||
|
return Object.keys(projected).length ? projected : undefined;
|
||||||
|
})(),
|
||||||
liveStream: this.buildLiveStreamView(id),
|
liveStream: this.buildLiveStreamView(id),
|
||||||
agentThinking: this.agentThinkingLive.get(id),
|
agentThinking: this.agentThinkingLive.get(id),
|
||||||
runActive: Boolean(s.runAbort),
|
runActive: Boolean(s.runAbort),
|
||||||
|
|||||||
@@ -613,8 +613,13 @@ export async function handleSettingsApi(
|
|||||||
activate: body.activate,
|
activate: body.activate,
|
||||||
creationDefault: body.creationDefault,
|
creationDefault: body.creationDefault,
|
||||||
});
|
});
|
||||||
|
const reboundSessions =
|
||||||
|
body.activate === false
|
||||||
|
? 0
|
||||||
|
: sessionManager.rebindPlayOpeningsForActivePersona();
|
||||||
json(res, 201, {
|
json(res, 201, {
|
||||||
...personasPayload({ persona }),
|
...personasPayload({ persona }),
|
||||||
|
reboundSessions,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
json(res, 400, {
|
json(res, 400, {
|
||||||
@@ -634,8 +639,11 @@ export async function handleSettingsApi(
|
|||||||
if (activate && req.method === "POST") {
|
if (activate && req.method === "POST") {
|
||||||
try {
|
try {
|
||||||
const persona = setActivePersonaId(id);
|
const persona = setActivePersonaId(id);
|
||||||
|
const reboundSessions =
|
||||||
|
sessionManager.rebindPlayOpeningsForActivePersona();
|
||||||
json(res, 200, {
|
json(res, 200, {
|
||||||
...personasPayload({ persona, activeId: persona.id }),
|
...personasPayload({ persona, activeId: persona.id }),
|
||||||
|
reboundSessions,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
json(res, 404, {
|
json(res, 404, {
|
||||||
@@ -653,8 +661,13 @@ export async function handleSettingsApi(
|
|||||||
creationDefault?: boolean;
|
creationDefault?: boolean;
|
||||||
};
|
};
|
||||||
const persona = updatePersona(id, body);
|
const persona = updatePersona(id, body);
|
||||||
|
const reboundSessions =
|
||||||
|
getActivePersona()?.id === persona.id
|
||||||
|
? sessionManager.rebindPlayOpeningsForActivePersona()
|
||||||
|
: 0;
|
||||||
json(res, 200, {
|
json(res, 200, {
|
||||||
...personasPayload({ persona }),
|
...personasPayload({ persona }),
|
||||||
|
reboundSessions,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
@@ -666,8 +679,11 @@ export async function handleSettingsApi(
|
|||||||
if (req.method === "DELETE") {
|
if (req.method === "DELETE") {
|
||||||
try {
|
try {
|
||||||
deletePersona(id);
|
deletePersona(id);
|
||||||
|
const reboundSessions =
|
||||||
|
sessionManager.rebindPlayOpeningsForActivePersona();
|
||||||
json(res, 200, {
|
json(res, 200, {
|
||||||
...personasPayload({ ok: true }),
|
...personasPayload({ ok: true }),
|
||||||
|
reboundSessions,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
|||||||
@@ -230,6 +230,54 @@ const server = createServer(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && sub === "/listen/consume") {
|
||||||
|
const body = JSON.parse(await readBody(req)) as {
|
||||||
|
consumedChars?: number;
|
||||||
|
};
|
||||||
|
const n = Number(body.consumedChars);
|
||||||
|
if (!Number.isFinite(n) || n < 0) {
|
||||||
|
json(res, 400, { error: "需要 consumedChars ≥ 0" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const view = sessionManager.markListenSerialConsumed(sessionId, n);
|
||||||
|
json(res, 200, view);
|
||||||
|
} catch (err) {
|
||||||
|
json(res, 400, {
|
||||||
|
error: err instanceof Error ? err.message : "进度更新失败",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && sub === "/listen/tts") {
|
||||||
|
const sectionId = (url.searchParams.get("sectionId") ?? "").trim();
|
||||||
|
if (!sectionId) {
|
||||||
|
json(res, 400, { error: "需要 sectionId" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const audio = await sessionManager.synthesizeListenSection(
|
||||||
|
sessionId,
|
||||||
|
sectionId,
|
||||||
|
);
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": audio.contentType,
|
||||||
|
"Content-Length": audio.audio.length,
|
||||||
|
"Cache-Control": audio.cached ? "private, max-age=86400" : "no-store",
|
||||||
|
"X-TTS-Provider": audio.provider,
|
||||||
|
"X-TTS-Voice": audio.voice,
|
||||||
|
"X-TTS-Cached": audio.cached ? "1" : "0",
|
||||||
|
});
|
||||||
|
res.end(audio.audio);
|
||||||
|
} catch (err) {
|
||||||
|
json(res, 400, {
|
||||||
|
error: err instanceof Error ? err.message : "语音合成失败",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "POST" && sub === "/answers") {
|
if (req.method === "POST" && sub === "/answers") {
|
||||||
const body = JSON.parse(await readBody(req)) as {
|
const body = JSON.parse(await readBody(req)) as {
|
||||||
answers?: Array<{
|
answers?: Array<{
|
||||||
|
|||||||
@@ -39,7 +39,22 @@ export type PersistedChatMessage = {
|
|||||||
messages: Array<{ role: string; content: string }>;
|
messages: Array<{ role: string; content: string }>;
|
||||||
charCount: number;
|
charCount: number;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
segments?: Array<{ tag: string; chars: number; hash: string }>;
|
||||||
};
|
};
|
||||||
|
/** 各次请求的标签轮廓,不含正文 */
|
||||||
|
requestOutlines?: Array<{
|
||||||
|
at: string;
|
||||||
|
caller: string;
|
||||||
|
step?: string;
|
||||||
|
turnId?: string;
|
||||||
|
model?: string;
|
||||||
|
promptTokens?: number;
|
||||||
|
completionTokens?: number;
|
||||||
|
cachedTokens?: number;
|
||||||
|
cacheMissTokens?: number;
|
||||||
|
cacheWriteTokens?: number;
|
||||||
|
segments: Array<{ tag: string; chars: number; hash: string }>;
|
||||||
|
}>;
|
||||||
branchGroupId?: string;
|
branchGroupId?: string;
|
||||||
branchIndex?: number;
|
branchIndex?: number;
|
||||||
branchTotal?: number;
|
branchTotal?: number;
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
/** 进料/创作方式:一书一方式,目录按此分夹 */
|
||||||
|
export type BookCreationMode = "recipe" | "dictate" | "listen_serial";
|
||||||
|
|
||||||
export type BookProject = {
|
export type BookProject = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
/** 用户选定或 agent 确认的 skill 包 */
|
/** 用户选定或 agent 确认的 skill 包 */
|
||||||
activeSkillId?: string;
|
activeSkillId?: string;
|
||||||
activeSkillName?: string;
|
activeSkillName?: string;
|
||||||
|
/** 新建时选定;缺省视为 dictate(旧书) */
|
||||||
|
creationMode?: BookCreationMode;
|
||||||
preview: string;
|
preview: string;
|
||||||
sessionIds: string[];
|
sessionIds: string[];
|
||||||
/** 最近一次持久化的 sessionId,用于续作 */
|
/** 最近一次持久化的 sessionId,用于续作 */
|
||||||
@@ -22,6 +27,7 @@ export type BookSummary = Pick<
|
|||||||
| "title"
|
| "title"
|
||||||
| "activeSkillId"
|
| "activeSkillId"
|
||||||
| "activeSkillName"
|
| "activeSkillName"
|
||||||
|
| "creationMode"
|
||||||
| "preview"
|
| "preview"
|
||||||
| "updatedAt"
|
| "updatedAt"
|
||||||
| "orchestratorId"
|
| "orchestratorId"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ describe("display-labels", () => {
|
|||||||
it("maps creation intake modes and recipe display names", () => {
|
it("maps creation intake modes and recipe display names", () => {
|
||||||
expect(displayCreationModeLabel("recipe")).toBe("工序编排");
|
expect(displayCreationModeLabel("recipe")).toBe("工序编排");
|
||||||
expect(displayCreationModeLabel("dictate")).toBe("对话落盘");
|
expect(displayCreationModeLabel("dictate")).toBe("对话落盘");
|
||||||
|
expect(displayCreationModeLabel("listen_serial")).toBe("听书连载");
|
||||||
expect(displaySkillPackLabel("world-simulator")).toBe("回合推演");
|
expect(displaySkillPackLabel("world-simulator")).toBe("回合推演");
|
||||||
expect(displaySkillPackLabel("快穿短局")).toBe("快穿短局");
|
expect(displaySkillPackLabel("快穿短局")).toBe("快穿短局");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||||
|
import {
|
||||||
|
applyPlayAuxLlmBubbles,
|
||||||
|
isPlayAuxLlmBubblesEnabled,
|
||||||
|
isPlayAuxLlmOutput,
|
||||||
|
} from "../web/feed-prefs.js";
|
||||||
|
|
||||||
|
describe("play aux LLM bubbles", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
applyPlayAuxLlmBubbles(false);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
applyPlayAuxLlmBubbles(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores the more-menu preference", () => {
|
||||||
|
expect(isPlayAuxLlmBubblesEnabled()).toBe(false);
|
||||||
|
applyPlayAuxLlmBubbles(true);
|
||||||
|
expect(isPlayAuxLlmBubblesEnabled()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats auditor worker_output as aux and world-simulator as main", () => {
|
||||||
|
const play = { lifecycleStage: "play" };
|
||||||
|
expect(
|
||||||
|
isPlayAuxLlmOutput(
|
||||||
|
{ role: "assistant", kind: "worker_output", actor: "auditor" },
|
||||||
|
play,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isPlayAuxLlmOutput(
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
kind: "worker_output",
|
||||||
|
actor: "world-simulator",
|
||||||
|
title: "主世界层 · 产出",
|
||||||
|
},
|
||||||
|
play,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isPlayAuxLlmOutput(
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
kind: "worker_output",
|
||||||
|
actor: "narrator",
|
||||||
|
title: "叙事转述 · 产出",
|
||||||
|
},
|
||||||
|
play,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
+639
-53
File diff suppressed because it is too large
Load Diff
+473
-96
@@ -34,6 +34,11 @@ import {
|
|||||||
initMarkdownRender,
|
initMarkdownRender,
|
||||||
isMarkdownRenderEnabled,
|
isMarkdownRenderEnabled,
|
||||||
} from "./markdown.js";
|
} from "./markdown.js";
|
||||||
|
import {
|
||||||
|
applyPlayAuxLlmBubbles,
|
||||||
|
initPlayAuxLlmBubbles,
|
||||||
|
isPlayAuxLlmBubblesEnabled,
|
||||||
|
} from "./feed-prefs.js";
|
||||||
import {
|
import {
|
||||||
filterUsedApiProfiles,
|
filterUsedApiProfiles,
|
||||||
forgetApiProfileUsage,
|
forgetApiProfileUsage,
|
||||||
@@ -64,7 +69,35 @@ let pendingComposerDraft = "";
|
|||||||
let pendingComposerDraftSeq = 0;
|
let pendingComposerDraftSeq = 0;
|
||||||
/** 正在提交底栏:重绘时不要把已捕获的正文再塞进 pending */
|
/** 正在提交底栏:重绘时不要把已捕获的正文再塞进 pending */
|
||||||
let skipComposerStash = false;
|
let skipComposerStash = false;
|
||||||
|
/** @type {{ level: "root" } | { level: "mode"; mode: string } | { level: "book"; mode: string; bookId: string }} */
|
||||||
let sidebarNav = { level: "root" };
|
let sidebarNav = { level: "root" };
|
||||||
|
|
||||||
|
const CREATION_MODE_FOLDERS = [
|
||||||
|
{ id: "dictate", name: "对话落盘" },
|
||||||
|
{ id: "listen_serial", name: "听书连载" },
|
||||||
|
{ id: "recipe", name: "工序编排" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function bookCreationMode(book) {
|
||||||
|
const m = book?.creationMode;
|
||||||
|
if (m === "listen_serial" || m === "recipe" || m === "dictate") return m;
|
||||||
|
return "dictate";
|
||||||
|
}
|
||||||
|
|
||||||
|
function creationModeFolderLabel(modeId) {
|
||||||
|
return CREATION_MODE_FOLDERS.find((f) => f.id === modeId)?.name ?? modeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function booksInCreationMode(modeId) {
|
||||||
|
return books.filter((b) => bookCreationMode(b) === modeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleCreationModeFolders() {
|
||||||
|
return CREATION_MODE_FOLDERS.filter((f) => {
|
||||||
|
if (f.id === "recipe") return booksInCreationMode("recipe").length > 0;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
let bookSelectMode = false;
|
let bookSelectMode = false;
|
||||||
let selectedBookIds = new Set();
|
let selectedBookIds = new Set();
|
||||||
let activePlaySaveId = null;
|
let activePlaySaveId = null;
|
||||||
@@ -128,7 +161,11 @@ function rememberWorkspaceSession() {
|
|||||||
mode,
|
mode,
|
||||||
playSaveId:
|
playSaveId:
|
||||||
mode === "play" ? activePlaySaveId || "play-working" : null,
|
mode === "play" ? activePlaySaveId || "play-working" : null,
|
||||||
navLevel: sidebarNav.level === "book" ? "book" : "root",
|
navLevel: sidebarNav.level,
|
||||||
|
creationMode:
|
||||||
|
sidebarNav.level === "mode" || sidebarNav.level === "book"
|
||||||
|
? sidebarNav.mode
|
||||||
|
: undefined,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -796,6 +833,21 @@ async function loadPersonas() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 换/改用户角色后重拉会话(服务端已按新 @玩家 重展开开场白) */
|
||||||
|
async function refreshSessionAfterPersonaChange() {
|
||||||
|
if (!sessionId) {
|
||||||
|
renderPersonaPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const view = await api(`/api/sessions/${encodeURIComponent(sessionId)}`);
|
||||||
|
renderSession(view);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
renderPersonaPicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function keepPersonaMenuOpen() {
|
function keepPersonaMenuOpen() {
|
||||||
const menu = $("persona-menu");
|
const menu = $("persona-menu");
|
||||||
if (menu) menu.open = true;
|
if (menu) menu.open = true;
|
||||||
@@ -848,6 +900,7 @@ function wirePersonaMenu() {
|
|||||||
});
|
});
|
||||||
personaEditingId = null;
|
personaEditingId = null;
|
||||||
await loadPersonas();
|
await loadPersonas();
|
||||||
|
await refreshSessionAfterPersonaChange();
|
||||||
keepPersonaMenuOpen();
|
keepPersonaMenuOpen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -865,6 +918,7 @@ function wirePersonaMenu() {
|
|||||||
});
|
});
|
||||||
personaEditingId = null;
|
personaEditingId = null;
|
||||||
await loadPersonas();
|
await loadPersonas();
|
||||||
|
await refreshSessionAfterPersonaChange();
|
||||||
keepPersonaMenuOpen();
|
keepPersonaMenuOpen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -875,6 +929,7 @@ function wirePersonaMenu() {
|
|||||||
});
|
});
|
||||||
personaEditingId = null;
|
personaEditingId = null;
|
||||||
await loadPersonas();
|
await loadPersonas();
|
||||||
|
await refreshSessionAfterPersonaChange();
|
||||||
keepPersonaMenuOpen();
|
keepPersonaMenuOpen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -905,10 +960,7 @@ function wirePersonaMenu() {
|
|||||||
}
|
}
|
||||||
personaEditingId = null;
|
personaEditingId = null;
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
const view = await api(
|
await refreshSessionAfterPersonaChange();
|
||||||
`/api/sessions/${encodeURIComponent(sessionId)}`,
|
|
||||||
);
|
|
||||||
renderSession(view);
|
|
||||||
} else {
|
} else {
|
||||||
renderPersonaPicker();
|
renderPersonaPicker();
|
||||||
}
|
}
|
||||||
@@ -1086,6 +1138,7 @@ function composerApiSectionHtml() {
|
|||||||
|
|
||||||
function composerMoreMenuHtml() {
|
function composerMoreMenuHtml() {
|
||||||
const on = isMarkdownRenderEnabled();
|
const on = isMarkdownRenderEnabled();
|
||||||
|
const auxOn = isPlayAuxLlmBubblesEnabled();
|
||||||
const wasOpen = $("composer-more-menu")?.hasAttribute("open");
|
const wasOpen = $("composer-more-menu")?.hasAttribute("open");
|
||||||
const title = composerMoreMenuTitle();
|
const title = composerMoreMenuTitle();
|
||||||
const showClearDictate =
|
const showClearDictate =
|
||||||
@@ -1127,6 +1180,19 @@ function composerMoreMenuHtml() {
|
|||||||
</span>
|
</span>
|
||||||
<span class="more-toggle-mark" aria-hidden="true"></span>
|
<span class="more-toggle-mark" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="more-menu-item more-toggle-item${auxOn ? " is-active" : ""}"
|
||||||
|
data-toggle-play-aux-llm
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
aria-checked="${auxOn ? "true" : "false"}"
|
||||||
|
>
|
||||||
|
<span class="more-theme-copy">
|
||||||
|
<span class="more-theme-name">非主世界层气泡</span>
|
||||||
|
<span class="more-theme-desc">旁观等 LLM 产出,便于查上下文与原文</span>
|
||||||
|
</span>
|
||||||
|
<span class="more-toggle-mark" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
${clearDictateItem}
|
${clearDictateItem}
|
||||||
</div>
|
</div>
|
||||||
</details>`;
|
</details>`;
|
||||||
@@ -1236,6 +1302,11 @@ function syncMarkdownMenuUi() {
|
|||||||
btn.classList.toggle("is-active", mdOn);
|
btn.classList.toggle("is-active", mdOn);
|
||||||
btn.setAttribute("aria-checked", mdOn ? "true" : "false");
|
btn.setAttribute("aria-checked", mdOn ? "true" : "false");
|
||||||
});
|
});
|
||||||
|
const auxOn = isPlayAuxLlmBubblesEnabled();
|
||||||
|
document.querySelectorAll("[data-toggle-play-aux-llm]").forEach((btn) => {
|
||||||
|
btn.classList.toggle("is-active", auxOn);
|
||||||
|
btn.setAttribute("aria-checked", auxOn ? "true" : "false");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncThemeMenuUi() {
|
function syncThemeMenuUi() {
|
||||||
@@ -1878,7 +1949,8 @@ async function fetchPlaySavesForBook(bookId, force = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setBookSelectMode(on, { keepSelection = false } = {}) {
|
function setBookSelectMode(on, { keepSelection = false } = {}) {
|
||||||
if (on && sidebarNav.level !== "root") return;
|
// 多选只在「方式夹内的作品列表」
|
||||||
|
if (on && sidebarNav.level !== "mode") return;
|
||||||
if (on) hideEnterPicker();
|
if (on) hideEnterPicker();
|
||||||
bookSelectMode = on;
|
bookSelectMode = on;
|
||||||
if (!on) selectedBookIds = new Set();
|
if (!on) selectedBookIds = new Set();
|
||||||
@@ -2076,6 +2148,9 @@ function getNavBook() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function catalogBookMeta(book) {
|
function catalogBookMeta(book) {
|
||||||
|
if (bookCreationMode(book) === "listen_serial") {
|
||||||
|
return "听书";
|
||||||
|
}
|
||||||
const saves = playSavesByBook.get(book.id);
|
const saves = playSavesByBook.get(book.id);
|
||||||
if (saves === undefined) {
|
if (saves === undefined) {
|
||||||
void fetchPlaySavesForBook(book.id);
|
void fetchPlaySavesForBook(book.id);
|
||||||
@@ -2156,7 +2231,14 @@ function renderPathBar() {
|
|||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
bar.innerHTML = "";
|
bar.innerHTML = "";
|
||||||
const book = getNavBook();
|
const book = getNavBook();
|
||||||
const inBook = Boolean(book);
|
const modeId =
|
||||||
|
sidebarNav.level === "mode" || sidebarNav.level === "book"
|
||||||
|
? sidebarNav.mode
|
||||||
|
: book
|
||||||
|
? bookCreationMode(book)
|
||||||
|
: null;
|
||||||
|
const inMode = Boolean(modeId);
|
||||||
|
const inBook = Boolean(book) && sidebarNav.level === "book";
|
||||||
|
|
||||||
$("rail-tabs")?.toggleAttribute("hidden", !(isWorkspaceNav() || Boolean(activeBookId)));
|
$("rail-tabs")?.toggleAttribute("hidden", !(isWorkspaceNav() || Boolean(activeBookId)));
|
||||||
|
|
||||||
@@ -2165,12 +2247,24 @@ function renderPathBar() {
|
|||||||
list.appendChild(
|
list.appendChild(
|
||||||
pathItem({
|
pathItem({
|
||||||
label: "目录",
|
label: "目录",
|
||||||
title: "作品目录",
|
title: "创作方式",
|
||||||
current: !inBook,
|
current: sidebarNav.level === "root",
|
||||||
onClick: () => showCatalog({ closeSession: false }),
|
onClick: () => showCatalog({ closeSession: false }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (inBook) {
|
if (inMode && modeId) {
|
||||||
|
const modeLabel = creationModeFolderLabel(modeId);
|
||||||
|
list.appendChild(pathSep());
|
||||||
|
list.appendChild(
|
||||||
|
pathItem({
|
||||||
|
label: modeLabel,
|
||||||
|
title: modeLabel,
|
||||||
|
current: sidebarNav.level === "mode",
|
||||||
|
onClick: () => navigateToCreationMode(modeId, { closeSession: false }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (inBook && book) {
|
||||||
const bookLabel = book.title?.trim() || "作品";
|
const bookLabel = book.title?.trim() || "作品";
|
||||||
list.appendChild(pathSep());
|
list.appendChild(pathSep());
|
||||||
list.appendChild(
|
list.appendChild(
|
||||||
@@ -2184,10 +2278,30 @@ function renderPathBar() {
|
|||||||
bar.appendChild(list);
|
bar.appendChild(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function navigateToCreationMode(modeId, { closeSession = false } = {}) {
|
||||||
|
if (!modeId) return;
|
||||||
|
hideEnterPicker();
|
||||||
|
if (closeSession) {
|
||||||
|
beginUiRequest();
|
||||||
|
stopLivePoll();
|
||||||
|
closeWorkspace();
|
||||||
|
}
|
||||||
|
sidebarNav = { level: "mode", mode: modeId };
|
||||||
|
document.body.dataset.navLevel = "mode";
|
||||||
|
bookSelectMode = false;
|
||||||
|
selectedBookIds = new Set();
|
||||||
|
setRailTab("books", { expand: true });
|
||||||
|
renderPathBar();
|
||||||
|
renderBookList();
|
||||||
|
if (!closeSession) rememberWorkspaceSession();
|
||||||
|
}
|
||||||
|
|
||||||
function navigateToBook(bookId) {
|
function navigateToBook(bookId) {
|
||||||
if (!bookId) return;
|
if (!bookId) return;
|
||||||
hideEnterPicker();
|
hideEnterPicker();
|
||||||
sidebarNav = { level: "book", bookId };
|
const book = books.find((b) => b.id === bookId);
|
||||||
|
const mode = bookCreationMode(book);
|
||||||
|
sidebarNav = { level: "book", mode, bookId };
|
||||||
document.body.dataset.navLevel = "book";
|
document.body.dataset.navLevel = "book";
|
||||||
bookSelectMode = false;
|
bookSelectMode = false;
|
||||||
selectedBookIds = new Set();
|
selectedBookIds = new Set();
|
||||||
@@ -2222,6 +2336,7 @@ function renderExplorerRow({
|
|||||||
actionClass,
|
actionClass,
|
||||||
onClick,
|
onClick,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onAdd,
|
||||||
onRename,
|
onRename,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
child,
|
child,
|
||||||
@@ -2248,20 +2363,26 @@ function renderExplorerRow({
|
|||||||
if (typeof ariaExpanded === "boolean") {
|
if (typeof ariaExpanded === "boolean") {
|
||||||
row.setAttribute("aria-expanded", ariaExpanded ? "true" : "false");
|
row.setAttribute("aria-expanded", ariaExpanded ? "true" : "false");
|
||||||
}
|
}
|
||||||
|
const addHtml = onAdd
|
||||||
|
? `<span class="explorer-add" role="button" tabindex="-1" title="${esc(onAdd.title || "新建")}" aria-label="${esc(onAdd.label || "新建")}">+</span>`
|
||||||
|
: "";
|
||||||
|
const delHtml = onDelete
|
||||||
|
? `<span class="explorer-del" role="button" tabindex="-1" aria-label="删除">×</span>`
|
||||||
|
: "";
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
${twistie ? `<span class="explorer-twist" data-twist>${expanded ? "▾" : "▸"}</span>` : ""}
|
${twistie ? `<span class="explorer-twist" data-twist>${expanded ? "▾" : "▸"}</span>` : ""}
|
||||||
<span class="explorer-main">
|
<span class="explorer-main">
|
||||||
<span class="explorer-name" title="${esc(name)}">${esc(name)}</span>
|
<span class="explorer-name" title="${esc(name)}">${esc(name)}</span>
|
||||||
${meta ? `<span class="explorer-meta" title="${esc(meta)}">${esc(meta)}</span>` : ""}
|
${meta ? `<span class="explorer-meta" title="${esc(meta)}">${esc(meta)}</span>` : ""}
|
||||||
</span>
|
</span>
|
||||||
${onDelete ? `<span class="explorer-del" role="button" tabindex="-1" aria-label="删除">×</span>` : ""}`;
|
${addHtml || delHtml ? `<span class="explorer-actions">${addHtml}${delHtml}</span>` : ""}`;
|
||||||
row.addEventListener("click", (e) => {
|
row.addEventListener("click", (e) => {
|
||||||
if (e.target.closest(".explorer-del") || e.target.closest("[data-twist]")) return;
|
if (e.target.closest(".explorer-actions") || e.target.closest("[data-twist]")) return;
|
||||||
onClick?.(e);
|
onClick?.(e);
|
||||||
});
|
});
|
||||||
if (onRename) {
|
if (onRename) {
|
||||||
row.addEventListener("dblclick", (e) => {
|
row.addEventListener("dblclick", (e) => {
|
||||||
if (e.target.closest(".explorer-del") || e.target.closest("[data-twist]")) return;
|
if (e.target.closest(".explorer-actions") || e.target.closest("[data-twist]")) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onRename();
|
onRename();
|
||||||
});
|
});
|
||||||
@@ -2270,6 +2391,10 @@ function renderExplorerRow({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
(onTwistie || onClick)?.();
|
(onTwistie || onClick)?.();
|
||||||
});
|
});
|
||||||
|
row.querySelector(".explorer-add")?.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onAdd?.onClick?.();
|
||||||
|
});
|
||||||
row.querySelector(".explorer-del")?.addEventListener("click", (e) => {
|
row.querySelector(".explorer-del")?.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDelete?.();
|
onDelete?.();
|
||||||
@@ -2337,17 +2462,6 @@ function renderSpecChildren(book, product, list) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendNewPlayFromSnapshot(book, list, instanceId) {
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.type = "button";
|
|
||||||
btn.className = "explorer-add child";
|
|
||||||
btn.textContent = "+";
|
|
||||||
btn.title = "从这份创作快照新开一局";
|
|
||||||
btn.setAttribute("aria-label", "新开一局");
|
|
||||||
btn.addEventListener("click", () => void startNewPlayForBook(book.id, instanceId));
|
|
||||||
list.appendChild(btn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendCurrentDesignRow(book, list) {
|
function appendCurrentDesignRow(book, list) {
|
||||||
const live =
|
const live =
|
||||||
book.id === activeBookId && Boolean(sessionId) && !isPlayNow();
|
book.id === activeBookId && Boolean(sessionId) && !isPlayNow();
|
||||||
@@ -2404,12 +2518,16 @@ function renderBookContents(book, list) {
|
|||||||
onTwistie: () => toggleSpecExpanded(book.id, product.id),
|
onTwistie: () => toggleSpecExpanded(book.id, product.id),
|
||||||
onRename: () => void renamePlaySave(book.id, product.id, product.label),
|
onRename: () => void renamePlaySave(book.id, product.id, product.label),
|
||||||
onDelete: () => void deletePlaySave(book.id, product.id, product.label, "规格"),
|
onDelete: () => void deletePlaySave(book.id, product.id, product.label, "规格"),
|
||||||
|
onAdd: {
|
||||||
|
label: "新开一局",
|
||||||
|
title: "从这份创作快照新开一局",
|
||||||
|
onClick: () => void startNewPlayForBook(book.id, product.id),
|
||||||
|
},
|
||||||
onContextMenu: (e) =>
|
onContextMenu: (e) =>
|
||||||
showSaveMenu(book.id, product.id, product.label, e.clientX, e.clientY),
|
showSaveMenu(book.id, product.id, product.label, e.clientX, e.clientY),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (expanded) renderSpecChildren(book, product, list);
|
if (expanded) renderSpecChildren(book, product, list);
|
||||||
appendNewPlayFromSnapshot(book, list, product.id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2417,11 +2535,6 @@ function renderBookList() {
|
|||||||
const list = $("book-list");
|
const list = $("book-list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
renderPathBar();
|
renderPathBar();
|
||||||
if (!books.length) {
|
|
||||||
list.innerHTML = `<p class="sidebar-empty">暂无作品<br><button type="button" class="btn-sm" id="btn-new-inline">+ 新建</button></p>`;
|
|
||||||
$("btn-new-inline")?.addEventListener("click", openNewBookDialog);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
|
|
||||||
if (sidebarNav.level === "book") {
|
if (sidebarNav.level === "book") {
|
||||||
@@ -2430,13 +2543,46 @@ function renderBookList() {
|
|||||||
renderBookContents(book, list);
|
renderBookContents(book, list);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sidebarNav = { level: "root" };
|
const fallbackMode = sidebarNav.mode || "dictate";
|
||||||
document.body.dataset.navLevel = "root";
|
sidebarNav = { level: "mode", mode: fallbackMode };
|
||||||
|
document.body.dataset.navLevel = "mode";
|
||||||
renderPathBar();
|
renderPathBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const book of books) {
|
if (sidebarNav.level === "root") {
|
||||||
|
for (const folder of visibleCreationModeFolders()) {
|
||||||
|
const count = booksInCreationMode(folder.id).length;
|
||||||
|
list.appendChild(
|
||||||
|
renderExplorerRow({
|
||||||
|
name: folder.name,
|
||||||
|
meta: count ? `${count} 部` : "空",
|
||||||
|
active: false,
|
||||||
|
selected: false,
|
||||||
|
actionClass: "explorer-folder",
|
||||||
|
onClick: () => navigateToCreationMode(folder.id),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mode:该方式下的作品
|
||||||
|
const modeId = sidebarNav.mode || "dictate";
|
||||||
|
const modeBooks = booksInCreationMode(modeId);
|
||||||
|
if (!modeBooks.length) {
|
||||||
|
const empty = document.createElement("p");
|
||||||
|
empty.className = "sidebar-empty";
|
||||||
|
empty.innerHTML = `此方式下暂无作品<br><button type="button" class="btn-sm" id="btn-new-inline">+ 新建</button>`;
|
||||||
|
list.appendChild(empty);
|
||||||
|
$("btn-new-inline")?.addEventListener("click", () => {
|
||||||
|
openNewBookDialog({ presetMode: modeId === "recipe" ? "dictate" : modeId });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const book of modeBooks) {
|
||||||
const selected = selectedBookIds.has(book.id);
|
const selected = selectedBookIds.has(book.id);
|
||||||
|
const listen = bookCreationMode(book) === "listen_serial";
|
||||||
list.appendChild(
|
list.appendChild(
|
||||||
renderExplorerRow({
|
renderExplorerRow({
|
||||||
name: book.title,
|
name: book.title,
|
||||||
@@ -2451,6 +2597,10 @@ function renderBookList() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
if (listen) {
|
||||||
|
void openBook(book.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (enterPickerBookId === book.id) {
|
if (enterPickerBookId === book.id) {
|
||||||
hideEnterPicker();
|
hideEnterPicker();
|
||||||
return;
|
return;
|
||||||
@@ -2460,7 +2610,7 @@ function renderBookList() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (enterPickerBookId && sidebarNav.level === "root") {
|
if (enterPickerBookId && sidebarNav.level === "mode") {
|
||||||
const row = list.querySelector(`[data-book-id="${enterPickerBookId}"]`);
|
const row = list.querySelector(`[data-book-id="${enterPickerBookId}"]`);
|
||||||
if (row) showEnterPicker(enterPickerBookId, row);
|
if (row) showEnterPicker(enterPickerBookId, row);
|
||||||
else hideEnterPicker();
|
else hideEnterPicker();
|
||||||
@@ -2503,6 +2653,7 @@ async function duplicateBookById(bookId, title) {
|
|||||||
title: data.book.title,
|
title: data.book.title,
|
||||||
activeSkillId: data.book.activeSkillId ?? data.book.orchestratorId,
|
activeSkillId: data.book.activeSkillId ?? data.book.orchestratorId,
|
||||||
activeSkillName: data.book.activeSkillName ?? data.book.orchestratorName,
|
activeSkillName: data.book.activeSkillName ?? data.book.orchestratorName,
|
||||||
|
creationMode: bookCreationMode(data.book),
|
||||||
preview: data.book.preview,
|
preview: data.book.preview,
|
||||||
updatedAt: data.book.updatedAt,
|
updatedAt: data.book.updatedAt,
|
||||||
orchestratorId: data.book.orchestratorId,
|
orchestratorId: data.book.orchestratorId,
|
||||||
@@ -2572,7 +2723,7 @@ function setupBookBoxSelect() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
list.addEventListener("mousedown", (e) => {
|
list.addEventListener("mousedown", (e) => {
|
||||||
if (sidebarNav.level !== "root" || e.button !== 0) return;
|
if (sidebarNav.level !== "mode" || e.button !== 0) return;
|
||||||
if (e.target.closest("#book-action-menu")) return;
|
if (e.target.closest("#book-action-menu")) return;
|
||||||
if (e.target.closest(".explorer-row")) return;
|
if (e.target.closest(".explorer-row")) return;
|
||||||
dragging = true;
|
dragging = true;
|
||||||
@@ -2855,7 +3006,7 @@ function renderEmpty() {
|
|||||||
document.body.dataset.navLevel = "root";
|
document.body.dataset.navLevel = "root";
|
||||||
bookSelectMode = false;
|
bookSelectMode = false;
|
||||||
selectedBookIds = new Set();
|
selectedBookIds = new Set();
|
||||||
closeWorkspace({ emptyCopy: "点击左侧 + 新建作品" });
|
closeWorkspace({ emptyCopy: "先选左侧创作方式,再新建作品" });
|
||||||
renderBookList();
|
renderBookList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3103,6 +3254,10 @@ function renderSession(view, loading = false) {
|
|||||||
const idx = lastView?.playAux?.selectedOpeningIndex ?? 0;
|
const idx = lastView?.playAux?.selectedOpeningIndex ?? 0;
|
||||||
void selectPlayOpening(idx, false);
|
void selectPlayOpening(idx, false);
|
||||||
},
|
},
|
||||||
|
getListenSerial: () => lastView?.listenSerial,
|
||||||
|
onListenConsume: (consumedChars) => void markListenConsumed(consumedChars),
|
||||||
|
onListenPlay: (sectionId, opts) => void playListenSection(sectionId, opts),
|
||||||
|
onListenStop: () => stopListenPlayback(),
|
||||||
});
|
});
|
||||||
renderComposer(view, loading);
|
renderComposer(view, loading);
|
||||||
rememberWorkspaceSession();
|
rememberWorkspaceSession();
|
||||||
@@ -3487,14 +3642,53 @@ async function loadBooks() {
|
|||||||
renderBookList();
|
renderBookList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openNewBookDialog() {
|
function resolveNewBookCreationMode(opts = {}) {
|
||||||
|
if (opts.presetMode === "listen_serial" || opts.presetMode === "dictate") {
|
||||||
|
return opts.presetMode;
|
||||||
|
}
|
||||||
|
if (sidebarNav.level === "mode" || sidebarNav.level === "book") {
|
||||||
|
const m = sidebarNav.mode;
|
||||||
|
if (m === "listen_serial") return "listen_serial";
|
||||||
|
if (m === "dictate" || m === "recipe") return "dictate";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNewBookDialog(opts = {}) {
|
||||||
$("input-book-title").value = "";
|
$("input-book-title").value = "";
|
||||||
|
const locked = resolveNewBookCreationMode(opts);
|
||||||
|
const modeField = $("field-creation-mode");
|
||||||
|
const modeSel = $("input-creation-mode");
|
||||||
|
const lockedInput = $("input-creation-mode-locked");
|
||||||
|
const desc = $("dialog-new-book-desc");
|
||||||
|
|
||||||
|
if (locked) {
|
||||||
|
if (modeField) modeField.hidden = true;
|
||||||
|
if (lockedInput) lockedInput.value = locked;
|
||||||
|
if (modeSel) modeSel.value = locked;
|
||||||
|
if (desc) {
|
||||||
|
desc.textContent = `将创建到「${creationModeFolderLabel(locked)}」,填写名称即可。`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 仍在目录根:须先选方式,或临时露出下拉
|
||||||
|
if (modeField) modeField.hidden = false;
|
||||||
|
if (lockedInput) lockedInput.value = "";
|
||||||
|
if (modeSel) modeSel.value = "dictate";
|
||||||
|
if (desc) desc.textContent = "先选创作方式,再填写名称。";
|
||||||
|
}
|
||||||
|
|
||||||
$("dialog-new-book").showModal();
|
$("dialog-new-book").showModal();
|
||||||
$("input-book-title").focus();
|
$("input-book-title").focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createBook() {
|
async function createBook() {
|
||||||
const title = $("input-book-title").value.trim() || "未命名作品";
|
const title = $("input-book-title").value.trim() || "未命名作品";
|
||||||
|
const locked = ($("input-creation-mode-locked")?.value || "").trim();
|
||||||
|
const creationMode =
|
||||||
|
locked === "listen_serial" ||
|
||||||
|
$("input-creation-mode")?.value === "listen_serial"
|
||||||
|
? "listen_serial"
|
||||||
|
: "dictate";
|
||||||
$("btn-create-book").disabled = true;
|
$("btn-create-book").disabled = true;
|
||||||
try {
|
try {
|
||||||
const data = await api("/api/books", {
|
const data = await api("/api/books", {
|
||||||
@@ -3502,15 +3696,20 @@ async function createBook() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title,
|
title,
|
||||||
orchestratorId: "world-simulator",
|
orchestratorId: "world-simulator",
|
||||||
creationMode: "dictate",
|
creationMode,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
$("dialog-new-book").close();
|
$("dialog-new-book").close();
|
||||||
if (data.book) books.unshift(data.book);
|
if (data.book) books.unshift(data.book);
|
||||||
activeBookId = data.book?.id;
|
activeBookId = data.book?.id;
|
||||||
sidebarNav = { level: "root" };
|
sidebarNav = {
|
||||||
document.body.dataset.navLevel = "root";
|
level: "book",
|
||||||
|
mode: creationMode,
|
||||||
|
bookId: data.book?.id,
|
||||||
|
};
|
||||||
|
document.body.dataset.navLevel = "book";
|
||||||
renderSession(data.session, false);
|
renderSession(data.session, false);
|
||||||
|
renderBookList();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -3520,6 +3719,7 @@ async function createBook() {
|
|||||||
|
|
||||||
async function openBook(bookId) {
|
async function openBook(bookId) {
|
||||||
const seq = beginUiRequest();
|
const seq = beginUiRequest();
|
||||||
|
navigateToBook(bookId);
|
||||||
activeBookId = bookId;
|
activeBookId = bookId;
|
||||||
if (lastView?.bookId === bookId) renderSession(lastView, true);
|
if (lastView?.bookId === bookId) renderSession(lastView, true);
|
||||||
try {
|
try {
|
||||||
@@ -3549,8 +3749,11 @@ async function deleteBookById(bookId) {
|
|||||||
if (activeBookId === bookId) activePlaySaveId = null;
|
if (activeBookId === bookId) activePlaySaveId = null;
|
||||||
hideBookMenu();
|
hideBookMenu();
|
||||||
const wasHere = activeBookId === bookId || sidebarNav.bookId === bookId;
|
const wasHere = activeBookId === bookId || sidebarNav.bookId === bookId;
|
||||||
|
const mode = bookCreationMode(book);
|
||||||
if (wasHere) {
|
if (wasHere) {
|
||||||
if (books.length) showCatalog();
|
if (booksInCreationMode(mode).length) {
|
||||||
|
navigateToCreationMode(mode, { closeSession: true });
|
||||||
|
} else if (books.length) showCatalog();
|
||||||
else renderEmpty();
|
else renderEmpty();
|
||||||
} else {
|
} else {
|
||||||
renderBookList();
|
renderBookList();
|
||||||
@@ -3586,6 +3789,113 @@ function defaultArchiveLabel() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function markListenConsumed(consumedChars) {
|
||||||
|
if (!sessionId) return;
|
||||||
|
try {
|
||||||
|
const view = await api(
|
||||||
|
`/api/sessions/${encodeURIComponent(sessionId)}/listen/consume`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ consumedChars }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
renderSession(view, false);
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message || "更新阅读进度失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {HTMLAudioElement | null} */
|
||||||
|
let listenAudioEl = null;
|
||||||
|
let listenPlayToken = 0;
|
||||||
|
|
||||||
|
function stopListenPlayback() {
|
||||||
|
listenPlayToken += 1;
|
||||||
|
if (listenAudioEl) {
|
||||||
|
listenAudioEl.pause();
|
||||||
|
listenAudioEl.removeAttribute("src");
|
||||||
|
listenAudioEl.load();
|
||||||
|
listenAudioEl = null;
|
||||||
|
}
|
||||||
|
document
|
||||||
|
.querySelectorAll(".listen-section.is-playing")
|
||||||
|
.forEach((el) => el.classList.remove("is-playing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenTtsUrl(sectionId) {
|
||||||
|
return `/api/sessions/${encodeURIComponent(sessionId)}/listen/tts?sectionId=${encodeURIComponent(sectionId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markListenSectionPlaying(sectionId) {
|
||||||
|
document
|
||||||
|
.querySelectorAll(".listen-section.is-playing")
|
||||||
|
.forEach((el) => el.classList.remove("is-playing"));
|
||||||
|
const el = document.querySelector(
|
||||||
|
`.listen-section[data-section-id="${CSS.escape(sectionId)}"]`,
|
||||||
|
);
|
||||||
|
if (el) {
|
||||||
|
el.classList.add("is-playing");
|
||||||
|
el.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} sectionId
|
||||||
|
* @param {{ continuous?: boolean }} [opts]
|
||||||
|
*/
|
||||||
|
async function playListenSection(sectionId, opts = {}) {
|
||||||
|
if (!sessionId || !sectionId) return;
|
||||||
|
const continuous = Boolean(opts.continuous);
|
||||||
|
stopListenPlayback();
|
||||||
|
const token = listenPlayToken;
|
||||||
|
|
||||||
|
const sections = lastView?.listenSerial?.sections ?? [];
|
||||||
|
let index = sections.findIndex((s) => s.id === sectionId);
|
||||||
|
if (index < 0) {
|
||||||
|
alert("未找到该节");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playOne = (id) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (token !== listenPlayToken) {
|
||||||
|
resolve("stopped");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
markListenSectionPlaying(id);
|
||||||
|
const audio = new Audio(listenTtsUrl(id));
|
||||||
|
listenAudioEl = audio;
|
||||||
|
audio.onended = () => resolve("ended");
|
||||||
|
audio.onerror = () => reject(new Error("播放失败(合成或网络错误)"));
|
||||||
|
void audio.play().catch((err) => reject(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (index >= 0 && index < sections.length) {
|
||||||
|
if (token !== listenPlayToken) return;
|
||||||
|
const cur = sections[index];
|
||||||
|
const result = await playOne(cur.id);
|
||||||
|
if (result !== "ended" || token !== listenPlayToken) return;
|
||||||
|
|
||||||
|
// 播完一节:把消费指针推到本节末尾
|
||||||
|
let endChars = 0;
|
||||||
|
for (let i = 0; i <= index; i += 1) {
|
||||||
|
endChars += String(sections[i]?.text || "").length;
|
||||||
|
}
|
||||||
|
await markListenConsumed(endChars);
|
||||||
|
|
||||||
|
if (!continuous) return;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (token === listenPlayToken) {
|
||||||
|
alert(err?.message || "朗读失败");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (token === listenPlayToken) stopListenPlayback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveCurrentInstance() {
|
async function saveCurrentInstance() {
|
||||||
if (!activeBookId || (!lastView?.playReady && !canOfferSaveProduct(lastView))) {
|
if (!activeBookId || (!lastView?.playReady && !canOfferSaveProduct(lastView))) {
|
||||||
return alert(
|
return alert(
|
||||||
@@ -3629,6 +3939,49 @@ async function showStepArtifactDialog(stepId, name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function firstCacheBreak(prev, next) {
|
||||||
|
const shared = Math.min(prev.length, next.length);
|
||||||
|
for (let i = 0; i < shared; i += 1) {
|
||||||
|
if (prev[i]?.tag !== next[i]?.tag || prev[i]?.hash !== next[i]?.hash) return i;
|
||||||
|
}
|
||||||
|
if (next.length > prev.length) return prev.length;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatOutline(outline, prev, index, total) {
|
||||||
|
const segments = outline.segments || [];
|
||||||
|
const breakAt = prev ? firstCacheBreak(prev, segments) : null;
|
||||||
|
const lines = segments.map((seg, i) => {
|
||||||
|
let mark = " ";
|
||||||
|
if (breakAt != null) {
|
||||||
|
if (breakAt < 0 || i < breakAt) mark = "已接上";
|
||||||
|
else if (i === breakAt) mark = "断开 ";
|
||||||
|
else mark = "之后 ";
|
||||||
|
}
|
||||||
|
const tag = seg.tag === "工具" ? `${seg.tag}〔tools 字段,不在消息正文〕` : seg.tag;
|
||||||
|
return `${String(i + 1).padStart(2, "0")} ${mark} ${tag} ${Number(seg.chars || 0).toLocaleString("zh-CN")}字 ${seg.hash}`;
|
||||||
|
});
|
||||||
|
let note = "这条消息里没有更早的请求可比。跨轮断点在用量页,按同一会话往上一条看。";
|
||||||
|
if (breakAt != null && breakAt < 0) {
|
||||||
|
note = "与上一次请求的前缀整段相同。";
|
||||||
|
} else if (breakAt != null) {
|
||||||
|
const tag = segments[breakAt]?.tag || "(上一条更长,本条到此结束)";
|
||||||
|
note = `前 ${breakAt} 段与上一次请求相同,从「${tag}」起前缀不再一致。`;
|
||||||
|
}
|
||||||
|
const cached = Number(outline.cachedTokens ?? 0);
|
||||||
|
if (breakAt != null && cached === 0 && (breakAt < 0 || breakAt > 0)) {
|
||||||
|
note += " 账单缓存是 0:前面的标签对得上,但上游没有算进缓存。";
|
||||||
|
}
|
||||||
|
const step = outline.step ? ` · ${outline.step}` : "";
|
||||||
|
return [
|
||||||
|
`======== ${index}/${total}${step} · ${outline.caller || "unknown"} · ${outline.model || "model?"} ========`,
|
||||||
|
`prompt ${outline.promptTokens ?? "—"} · 缓存 ${outline.cachedTokens ?? "—"} · 未缓存 ${outline.cacheMissTokens ?? "—"} · 写出 ${outline.completionTokens ?? "—"}`,
|
||||||
|
note,
|
||||||
|
"",
|
||||||
|
...lines,
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function showContextTraceDialog(messageId) {
|
function showContextTraceDialog(messageId) {
|
||||||
const msg = (lastView?.messages ?? []).find((m) => m.id === messageId);
|
const msg = (lastView?.messages ?? []).find((m) => m.id === messageId);
|
||||||
const review = lastView?.reviewArtifact;
|
const review = lastView?.reviewArtifact;
|
||||||
@@ -3642,59 +3995,63 @@ function showContextTraceDialog(messageId) {
|
|||||||
const meta = $("context-trace-meta");
|
const meta = $("context-trace-meta");
|
||||||
const body = $("context-trace-body");
|
const body = $("context-trace-body");
|
||||||
if (!dlg || !meta || !body) return;
|
if (!dlg || !meta || !body) return;
|
||||||
if (!trace?.messages?.length) {
|
const subject = msg || review || {};
|
||||||
alert("这条消息没有保存请求上下文(可能已被修剪,或当时未开启保存)");
|
const stored = msg?.requestOutlines?.length
|
||||||
|
? msg.requestOutlines
|
||||||
|
: trace?.segments?.length
|
||||||
|
? [{
|
||||||
|
at: trace.createdAt,
|
||||||
|
caller: trace.caller,
|
||||||
|
model: trace.model,
|
||||||
|
segments: trace.segments,
|
||||||
|
promptTokens: subject.tokenUsage?.promptTokens,
|
||||||
|
completionTokens: subject.tokenUsage?.completionTokens,
|
||||||
|
cachedTokens: subject.tokenUsage?.cachedTokens,
|
||||||
|
cacheMissTokens: subject.tokenUsage?.cacheMissTokens,
|
||||||
|
}]
|
||||||
|
: [];
|
||||||
|
const full = formatTraceMessages(trace);
|
||||||
|
if (!stored.length && !full) {
|
||||||
|
alert("这条消息没有保存请求上下文");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const subject = msg || review || {};
|
let prev = null;
|
||||||
const requestChars =
|
const blocks = stored.map((outline, index) => {
|
||||||
trace.charCount ??
|
const text = formatOutline(outline, prev, index + 1, stored.length);
|
||||||
trace.messages.reduce((sum, item) => sum + String(item.content ?? "").length, 0);
|
prev = outline.segments || [];
|
||||||
const usage = subject.tokenUsage;
|
return text;
|
||||||
const responseBody = String(subject.body ?? subject.text ?? "").trim();
|
|
||||||
const thinking = String(subject.thinking ?? "").trim();
|
|
||||||
const requestLog = trace.messages.map((item, index) => {
|
|
||||||
const content = String(item.content ?? "");
|
|
||||||
const part = String(index + 1).padStart(2, "0");
|
|
||||||
return `======== REQUEST ${part}/${trace.messages.length} · ${item.role} · ${content.length.toLocaleString("zh-CN")} chars ========\n${content}`;
|
|
||||||
});
|
});
|
||||||
const responseLog = [
|
const calls = stored.length;
|
||||||
thinking
|
const segments = stored.reduce((sum, item) => sum + (item.segments?.length || 0), 0);
|
||||||
? `======== RESPONSE THINKING · ${thinking.length.toLocaleString("zh-CN")} chars ========\n${thinking}`
|
const title = $("context-trace-title");
|
||||||
: "",
|
if (title) title.textContent = full ? "完整上下文" : "请求分段";
|
||||||
responseBody
|
|
||||||
? `======== RESPONSE BODY · ${responseBody.length.toLocaleString("zh-CN")} chars ========\n${responseBody}`
|
|
||||||
: "",
|
|
||||||
].filter(Boolean);
|
|
||||||
const tokenMeta = usage?.totalTokens
|
|
||||||
? ` · ${Number(usage.totalTokens).toLocaleString("zh-CN")} tokens`
|
|
||||||
: "";
|
|
||||||
meta.textContent = [
|
meta.textContent = [
|
||||||
subject.kind || "message",
|
subject.kind || "message",
|
||||||
subject.actor || trace.caller || "unknown",
|
subject.actor || stored[0]?.caller || trace?.caller || "unknown",
|
||||||
trace.model || usage?.model || "model?",
|
`${calls} 次请求`,
|
||||||
`${trace.messages.length} 段请求`,
|
`${segments} 段`,
|
||||||
`${(requestChars / 1024).toFixed(1)} KB`,
|
full ? "下方是发给模型的消息" : "这次只留下分段标签",
|
||||||
].join(" · ") + tokenMeta;
|
].join(" · ");
|
||||||
body.textContent = [
|
const outline = $("context-trace-outline");
|
||||||
`MESSAGE ${messageId}`,
|
const messages = $("context-trace-messages");
|
||||||
`KIND ${subject.kind || "unknown"}`,
|
if (outline) outline.textContent = blocks.join("\n\n");
|
||||||
`ACTOR ${subject.actor || "-"}`,
|
body.textContent = full;
|
||||||
`CALLER ${trace.caller || usage?.caller || "unknown"}`,
|
if (messages) messages.hidden = !full;
|
||||||
`MODEL ${trace.model || usage?.model || "model?"}`,
|
|
||||||
`CREATED ${trace.createdAt || subject.createdAt || "-"}`,
|
|
||||||
`REQUEST ${trace.messages.length} messages / ${requestChars.toLocaleString("zh-CN")} chars`,
|
|
||||||
trace.generation && Object.keys(trace.generation).length
|
|
||||||
? `GEN ${JSON.stringify(trace.generation)}`
|
|
||||||
: "GEN (none)",
|
|
||||||
usage?.totalTokens
|
|
||||||
? `TOKENS total=${usage.totalTokens} cached=${usage.cachedTokens ?? 0} miss=${usage.cacheMissTokens ?? 0}`
|
|
||||||
: "TOKENS not recorded",
|
|
||||||
"",
|
|
||||||
...requestLog,
|
|
||||||
...(responseLog.length ? ["", ...responseLog] : []),
|
|
||||||
].join("\n\n");
|
|
||||||
dlg.showModal();
|
dlg.showModal();
|
||||||
|
const scroll = $("context-trace-scroll");
|
||||||
|
if (scroll) scroll.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTraceMessages(trace) {
|
||||||
|
const messages = trace?.messages;
|
||||||
|
if (!Array.isArray(messages) || !messages.length) return "";
|
||||||
|
return messages
|
||||||
|
.map((message, index) => {
|
||||||
|
const role = message?.role || "message";
|
||||||
|
const content = typeof message?.content === "string" ? message.content : "";
|
||||||
|
return `----- ${index + 1}. ${role} -----\n${content}`;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
let comparePickerMessageId = null;
|
let comparePickerMessageId = null;
|
||||||
@@ -3896,9 +4253,14 @@ $("compare-results")?.addEventListener("click", (e) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("btn-copy-context")?.addEventListener("click", async () => {
|
$("btn-copy-context")?.addEventListener("click", async () => {
|
||||||
|
const outline = $("context-trace-outline")?.textContent ?? "";
|
||||||
const body = $("context-trace-body")?.textContent ?? "";
|
const body = $("context-trace-body")?.textContent ?? "";
|
||||||
|
const text = [
|
||||||
|
outline ? `【系统信息 · 对照给你看,不发给模型】\n${outline}` : "",
|
||||||
|
body ? `【发给模型的消息】\n${body}` : "",
|
||||||
|
].filter(Boolean).join("\n\n");
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(body);
|
await navigator.clipboard.writeText(text);
|
||||||
} catch {
|
} catch {
|
||||||
alert("复制失败");
|
alert("复制失败");
|
||||||
}
|
}
|
||||||
@@ -3915,7 +4277,14 @@ $("lifecycle-toggle")?.addEventListener("click", (e) => {
|
|||||||
if (stage === "play") enterPlayNow();
|
if (stage === "play") enterPlayNow();
|
||||||
else if (stage === "design") void setLifecycle("design");
|
else if (stage === "design") void setLifecycle("design");
|
||||||
});
|
});
|
||||||
$("btn-new-book")?.addEventListener("click", openNewBookDialog);
|
$("btn-new-book")?.addEventListener("click", () => {
|
||||||
|
const locked = resolveNewBookCreationMode();
|
||||||
|
if (!locked && sidebarNav.level === "root") {
|
||||||
|
alert("请先进入「对话落盘」或「听书连载」文件夹,再新建作品。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openNewBookDialog(locked ? { presetMode: locked } : {});
|
||||||
|
});
|
||||||
$("btn-cancel-new")?.addEventListener("click", () => $("dialog-new-book").close());
|
$("btn-cancel-new")?.addEventListener("click", () => $("dialog-new-book").close());
|
||||||
$("book-enter-pop")?.addEventListener("click", (e) => {
|
$("book-enter-pop")?.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -4124,6 +4493,14 @@ $("composer")?.addEventListener("click", (e) => {
|
|||||||
if (lastView && sessionId) renderSession(lastView, false);
|
if (lastView && sessionId) renderSession(lastView, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const auxBtn = e.target.closest("[data-toggle-play-aux-llm]");
|
||||||
|
if (auxBtn && menu.contains(auxBtn)) {
|
||||||
|
e.preventDefault();
|
||||||
|
applyPlayAuxLlmBubbles(!isPlayAuxLlmBubblesEnabled());
|
||||||
|
syncMarkdownMenuUi();
|
||||||
|
if (lastView && sessionId) renderSession(lastView, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const reloadBtn = e.target.closest("[data-reload-profiles]");
|
const reloadBtn = e.target.closest("[data-reload-profiles]");
|
||||||
if (reloadBtn && menu.contains(reloadBtn)) {
|
if (reloadBtn && menu.contains(reloadBtn)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -4197,11 +4574,11 @@ async function restoreWorkspaceSession() {
|
|||||||
: null;
|
: null;
|
||||||
if (saved.navLevel === "book") {
|
if (saved.navLevel === "book") {
|
||||||
navigateToBook(bookId);
|
navigateToBook(bookId);
|
||||||
|
} else if (saved.navLevel === "mode" && saved.creationMode) {
|
||||||
|
navigateToCreationMode(saved.creationMode, { closeSession: false });
|
||||||
} else {
|
} else {
|
||||||
sidebarNav = { level: "root" };
|
const mode = bookCreationMode(books.find((b) => b.id === bookId));
|
||||||
document.body.dataset.navLevel = "root";
|
navigateToCreationMode(mode, { closeSession: false });
|
||||||
renderPathBar();
|
|
||||||
renderBookList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saved.sessionId) {
|
if (saved.sessionId) {
|
||||||
@@ -4243,6 +4620,7 @@ async function init() {
|
|||||||
initColorTheme();
|
initColorTheme();
|
||||||
initUiFontScale();
|
initUiFontScale();
|
||||||
initMarkdownRender();
|
initMarkdownRender();
|
||||||
|
initPlayAuxLlmBubbles();
|
||||||
syncThemeMenuUi();
|
syncThemeMenuUi();
|
||||||
try {
|
try {
|
||||||
await Promise.all([loadBooks(), loadPersonas(), loadApiProfiles()]);
|
await Promise.all([loadBooks(), loadPersonas(), loadApiProfiles()]);
|
||||||
@@ -4251,7 +4629,6 @@ async function init() {
|
|||||||
if (!restored) showCatalog();
|
if (!restored) showCatalog();
|
||||||
} else {
|
} else {
|
||||||
renderEmpty();
|
renderEmpty();
|
||||||
openNewBookDialog();
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
$("status-text").textContent = "加载失败";
|
$("status-text").textContent = "加载失败";
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export function displayCreationModeLabel(id) {
|
|||||||
const map = {
|
const map = {
|
||||||
recipe: "工序编排",
|
recipe: "工序编排",
|
||||||
dictate: "对话落盘",
|
dictate: "对话落盘",
|
||||||
|
listen_serial: "听书连载",
|
||||||
};
|
};
|
||||||
return map[id] ?? id;
|
return map[id] ?? id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/** 游玩主列:非主世界层 LLM 气泡(旁观 / 其它执行单元)显示偏好 */
|
||||||
|
|
||||||
|
const AUX_LLM_STORAGE_KEY = "wa-play-aux-llm-bubbles";
|
||||||
|
const AUX_LLM_ATTR = "data-play-aux-llm-bubbles";
|
||||||
|
|
||||||
|
/** @type {boolean | null} */
|
||||||
|
let auxLlmMemory = null;
|
||||||
|
|
||||||
|
export function getStoredPlayAuxLlmBubbles() {
|
||||||
|
if (auxLlmMemory != null) return auxLlmMemory;
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(AUX_LLM_STORAGE_KEY) === "1";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPlayAuxLlmBubblesEnabled() {
|
||||||
|
if (auxLlmMemory != null) return auxLlmMemory;
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
const attr = document.documentElement?.getAttribute(AUX_LLM_ATTR);
|
||||||
|
if (attr === "1" || attr === "0") return attr === "1";
|
||||||
|
}
|
||||||
|
return getStoredPlayAuxLlmBubbles();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyPlayAuxLlmBubbles(enabled) {
|
||||||
|
const on = Boolean(enabled);
|
||||||
|
auxLlmMemory = on;
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.documentElement?.setAttribute(AUX_LLM_ATTR, on ? "1" : "0");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(AUX_LLM_STORAGE_KEY, on ? "1" : "0");
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return on;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initPlayAuxLlmBubbles() {
|
||||||
|
auxLlmMemory = null;
|
||||||
|
return applyPlayAuxLlmBubbles(getStoredPlayAuxLlmBubbles());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlayView(view) {
|
||||||
|
return Boolean(view?.playLayerActive || view?.lifecycleStage === "play");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlayHousekeepingMessage(msg) {
|
||||||
|
const title = String(msg?.title ?? "");
|
||||||
|
const text = String(msg?.text ?? msg?.body ?? "").trim();
|
||||||
|
if (title === "上下文已压缩" || title === "阶段机") return true;
|
||||||
|
if (/^\[上下文已压缩\]/.test(text)) return true;
|
||||||
|
if (/^\[裁决合并\]/.test(text)) return true;
|
||||||
|
if (/^\[旁观维护\]/.test(text)) return true;
|
||||||
|
if (/归档\s*\d+\s*个过程 tag/.test(text)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRenderPlayPresent(msg, view) {
|
||||||
|
if (!isPlayView(view)) return false;
|
||||||
|
if (msg?.role === "user") return false;
|
||||||
|
const kind = msg?.kind ?? "";
|
||||||
|
if (kind === "worker_questions" || kind === "orchestrator_thinking") return false;
|
||||||
|
if (isPlayHousekeepingMessage(msg)) return false;
|
||||||
|
const actor = String(msg?.actor ?? "");
|
||||||
|
const title = String(msg?.title ?? "");
|
||||||
|
return (
|
||||||
|
actor === "narrator" ||
|
||||||
|
actor === "round-present" ||
|
||||||
|
actor === "world-simulator" ||
|
||||||
|
/用户展示|开场白/.test(title)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMainWorldPlayActor(actor) {
|
||||||
|
const id = String(actor ?? "").trim();
|
||||||
|
return id === "world-simulator" || id === "gm";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 主世界层以外的游玩 LLM 产出(旁观维护等),可进主列气泡查上下文 / 原文 */
|
||||||
|
export function isPlayAuxLlmOutput(msg, view) {
|
||||||
|
if (!isPlayView(view) || msg?.role === "user") return false;
|
||||||
|
if (isPlayHousekeepingMessage(msg)) return false;
|
||||||
|
if (shouldRenderPlayPresent(msg, view)) return false;
|
||||||
|
const kind = msg?.kind ?? "";
|
||||||
|
if (kind === "worker_output") {
|
||||||
|
return !isMainWorldPlayActor(msg.actor);
|
||||||
|
}
|
||||||
|
if (msg?.contextTrace || msg?.requestOutlines?.length) {
|
||||||
|
return !isMainWorldPlayActor(msg.actor);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
+23
-4
@@ -10,7 +10,7 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&family=Noto+Serif+SC:wght@500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&family=Noto+Serif+SC:wght@500;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="/styles.css?v=20260930-present-preview" />
|
<link rel="stylesheet" href="/styles.css?v=20261002-context-system" />
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
@@ -196,11 +196,19 @@
|
|||||||
<dialog id="dialog-new-book" class="dialog">
|
<dialog id="dialog-new-book" class="dialog">
|
||||||
<form method="dialog" id="form-new-book">
|
<form method="dialog" id="form-new-book">
|
||||||
<h2>新建作品</h2>
|
<h2>新建作品</h2>
|
||||||
<p class="dialog-desc">创建后直接说你想写什么;助手会在对话中判断合适的创作路径。</p>
|
<p class="dialog-desc" id="dialog-new-book-desc">填写名称即可创建。</p>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
名称
|
名称
|
||||||
<input id="input-book-title" type="text" placeholder="未命名作品" maxlength="80" autofocus />
|
<input id="input-book-title" type="text" placeholder="未命名作品" maxlength="80" autofocus />
|
||||||
</label>
|
</label>
|
||||||
|
<label class="field" id="field-creation-mode" hidden>
|
||||||
|
创作方式
|
||||||
|
<select id="input-creation-mode">
|
||||||
|
<option value="dictate" selected>对话落盘(可玩规格)</option>
|
||||||
|
<option value="listen_serial">听书连载(连续消费)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<input type="hidden" id="input-creation-mode-locked" value="" />
|
||||||
<div class="dialog-actions">
|
<div class="dialog-actions">
|
||||||
<button type="button" class="btn" value="cancel" id="btn-cancel-new">取消</button>
|
<button type="button" class="btn" value="cancel" id="btn-cancel-new">取消</button>
|
||||||
<button type="submit" class="btn btn-primary" id="btn-create-book">创建</button>
|
<button type="submit" class="btn btn-primary" id="btn-create-book">创建</button>
|
||||||
@@ -235,6 +243,7 @@
|
|||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
<menu id="msg-action-menu" class="context-menu" hidden>
|
<menu id="msg-action-menu" class="context-menu" hidden>
|
||||||
|
<button type="button" class="context-menu-item" id="msg-menu-raw" data-msg-menu-action="view-raw">查看原文</button>
|
||||||
<button type="button" class="context-menu-item" id="msg-menu-context" data-msg-menu-action="view-context" hidden>查看上下文</button>
|
<button type="button" class="context-menu-item" id="msg-menu-context" data-msg-menu-action="view-context" hidden>查看上下文</button>
|
||||||
<button type="button" class="context-menu-item" id="msg-menu-compare" data-msg-menu-action="compare" hidden>对比</button>
|
<button type="button" class="context-menu-item" id="msg-menu-compare" data-msg-menu-action="compare" hidden>对比</button>
|
||||||
<button type="button" class="context-menu-item" id="msg-menu-edit" data-msg-menu-action="edit">编辑</button>
|
<button type="button" class="context-menu-item" id="msg-menu-edit" data-msg-menu-action="edit">编辑</button>
|
||||||
@@ -291,9 +300,19 @@
|
|||||||
|
|
||||||
<dialog id="dialog-context-trace" class="dialog dialog-wide" aria-labelledby="context-trace-title">
|
<dialog id="dialog-context-trace" class="dialog dialog-wide" aria-labelledby="context-trace-title">
|
||||||
<form method="dialog" class="context-trace-form">
|
<form method="dialog" class="context-trace-form">
|
||||||
<h2 id="context-trace-title">上下文调用日志</h2>
|
<h2 id="context-trace-title">请求分段</h2>
|
||||||
<p class="dialog-desc" id="context-trace-meta"></p>
|
<p class="dialog-desc" id="context-trace-meta"></p>
|
||||||
|
<div class="context-trace-scroll" id="context-trace-scroll">
|
||||||
|
<section class="context-trace-system" id="context-trace-system">
|
||||||
|
<p class="context-trace-kicker">系统信息</p>
|
||||||
|
<p class="dialog-desc" id="context-trace-system-note">用量、断点、分段标签和哈希只给你对照,这段文字本身不会发给模型。「工具」是单独的 tools 字段,不在消息正文里。</p>
|
||||||
|
<pre id="context-trace-outline" class="context-trace-body context-trace-outline"></pre>
|
||||||
|
</section>
|
||||||
|
<section id="context-trace-messages">
|
||||||
|
<p class="context-trace-kicker">发给模型的消息</p>
|
||||||
<pre id="context-trace-body" class="context-trace-body"></pre>
|
<pre id="context-trace-body" class="context-trace-body"></pre>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
<div class="dialog-actions">
|
<div class="dialog-actions">
|
||||||
<button type="button" class="btn" id="btn-copy-context">复制全文</button>
|
<button type="button" class="btn" id="btn-copy-context">复制全文</button>
|
||||||
<button type="submit" class="btn btn-primary" value="close">关闭</button>
|
<button type="submit" class="btn btn-primary" value="close">关闭</button>
|
||||||
@@ -324,6 +343,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script type="module" src="/app.js?v=20260928-session-restore"></script>
|
<script type="module" src="/app.js?v=20261007-view-raw"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+68
-2
@@ -30,11 +30,48 @@ html[data-embed="1"] .st-main {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-pack {
|
.mod-count {
|
||||||
margin: 0 8px 10px;
|
margin: 0 8px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--st-muted);
|
color: var(--st-muted);
|
||||||
word-break: break-all;
|
}
|
||||||
|
|
||||||
|
.mod-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-group-label {
|
||||||
|
margin: 8px 8px 0;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--st-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-group-hint {
|
||||||
|
margin: 2px 8px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--st-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-item-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--st-border);
|
||||||
|
color: var(--st-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-filter {
|
.mod-filter {
|
||||||
@@ -78,6 +115,15 @@ html[data-embed="1"] .st-main {
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mod-item:focus-visible,
|
||||||
|
.mod-chip:focus-visible,
|
||||||
|
.mod-raw-toggle:focus-visible,
|
||||||
|
.mod-section-nav a:focus-visible,
|
||||||
|
.mod-filter input:focus-visible {
|
||||||
|
outline: 2px solid var(--st-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.mod-list {
|
.mod-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -192,6 +238,26 @@ html[data-embed="1"] .st-main {
|
|||||||
|
|
||||||
.mod-read > .st-status {
|
.mod-read > .st-status {
|
||||||
margin: 16px 28px 0;
|
margin: 16px 28px 0;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-meta-row + .mod-meta-row {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--st-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-meta-row strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-meta-row p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-read > .st-body {
|
.mod-read > .st-body {
|
||||||
|
|||||||
+7
-7
@@ -12,7 +12,7 @@
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<title>技能池 · Writing Agent</title>
|
<title>节点 · Writing Agent</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
|
|
||||||
<div class="st-shell mod-shell">
|
<div class="st-shell mod-shell">
|
||||||
<aside class="st-rail mod-rail" aria-label="技能列表">
|
<aside class="st-rail mod-rail" aria-label="技能列表">
|
||||||
<p class="st-rail-label">备选技能</p>
|
<p class="st-rail-label">节点</p>
|
||||||
<p class="mod-pack" id="pack-label">加载中…</p>
|
<p class="mod-count" id="pack-label">加载中…</p>
|
||||||
<div class="mod-filter">
|
<div class="mod-filter">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
@@ -56,15 +56,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav class="mod-list" id="mod-list"></nav>
|
<nav class="mod-list" id="mod-list"></nav>
|
||||||
<p class="st-rail-note">
|
<p class="st-rail-note">
|
||||||
创作助手根据本局设计清单从这里取用能力;本页只读浏览方法正文。
|
创作按本局方案从这些节点取方法。普通楼层和 0 层卡各用自己的呈现链;落档程序在开玩前由程序编译。本页只读。
|
||||||
</p>
|
</p>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="st-main mod-main">
|
<main class="st-main mod-main">
|
||||||
<header class="st-main-head">
|
<header class="st-main-head">
|
||||||
<div>
|
<div>
|
||||||
<h1 id="panel-title">技能池</h1>
|
<h1 id="panel-title">节点</h1>
|
||||||
<p class="st-sub" id="panel-subtitle">选择左侧一项查看方法块</p>
|
<p class="st-sub" id="panel-subtitle">选择左侧一项查看方法</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="st-main-actions">
|
<div class="st-main-actions">
|
||||||
<span class="mod-badge" id="status-badge" hidden></span>
|
<span class="mod-badge" id="status-badge" hidden></span>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<div class="mod-read">
|
<div class="mod-read">
|
||||||
<div class="st-status" id="meta-bar" hidden></div>
|
<div class="st-status" id="meta-bar" hidden></div>
|
||||||
<div class="st-body mod-body" id="mod-detail">
|
<div class="st-body mod-body" id="mod-detail">
|
||||||
<p class="mod-empty">从左侧选择一项技能</p>
|
<p class="mod-empty">从左侧选择一个节点</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+103
-19
@@ -5,6 +5,7 @@ const SECTION_LABELS = {
|
|||||||
principles: "原则",
|
principles: "原则",
|
||||||
probe: "追问",
|
probe: "追问",
|
||||||
output: "产物形状",
|
output: "产物形状",
|
||||||
|
score: "评分",
|
||||||
checklist: "自检",
|
checklist: "自检",
|
||||||
examples: "示例",
|
examples: "示例",
|
||||||
};
|
};
|
||||||
@@ -16,6 +17,7 @@ const SECTION_ORDER = [
|
|||||||
"principles",
|
"principles",
|
||||||
"probe",
|
"probe",
|
||||||
"output",
|
"output",
|
||||||
|
"score",
|
||||||
"checklist",
|
"checklist",
|
||||||
"examples",
|
"examples",
|
||||||
];
|
];
|
||||||
@@ -27,8 +29,20 @@ const STATUS_LABELS = {
|
|||||||
missing: "缺文件",
|
missing: "缺文件",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PRESENT_LABELS = {
|
||||||
|
message: "普通楼层",
|
||||||
|
zero_layer: "0 层卡",
|
||||||
|
};
|
||||||
|
|
||||||
|
const LAYER_LABELS = {
|
||||||
|
intermediate: "中间产物",
|
||||||
|
final: "最终产物",
|
||||||
|
};
|
||||||
|
|
||||||
/** @type {Array<any>} */
|
/** @type {Array<any>} */
|
||||||
let modulesCache = [];
|
let modulesCache = [];
|
||||||
|
/** @type {Array<{id: string, label: string, hint: string}>} */
|
||||||
|
let groupsCache = [];
|
||||||
let activeId = "";
|
let activeId = "";
|
||||||
let statusFilter = "";
|
let statusFilter = "";
|
||||||
let searchQuery = "";
|
let searchQuery = "";
|
||||||
@@ -62,23 +76,64 @@ function filteredModules() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowTags(module) {
|
||||||
|
const tags = [];
|
||||||
|
const mode = PRESENT_LABELS[module.presentMode];
|
||||||
|
if (mode) tags.push(mode);
|
||||||
|
if (module.repeatable) tags.push("可反复");
|
||||||
|
if (module.closer) tags.push("收口");
|
||||||
|
if (module.priorArtifact) tags.push("先验产物");
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
const list = document.getElementById("mod-list");
|
const list = document.getElementById("mod-list");
|
||||||
const items = filteredModules();
|
const items = filteredModules();
|
||||||
|
const count = document.getElementById("pack-label");
|
||||||
|
count.textContent =
|
||||||
|
items.length === modulesCache.length
|
||||||
|
? `${modulesCache.length} 个节点`
|
||||||
|
: `显示 ${items.length} / ${modulesCache.length}`;
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
list.innerHTML = `<p class="mod-empty" style="padding:8px">无匹配项</p>`;
|
list.innerHTML = `<p class="mod-empty">没有符合条件的节点</p>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.innerHTML = items
|
|
||||||
.map(
|
const byGroup = new Map(groupsCache.map((group) => [group.id, []]));
|
||||||
(m) => `<button type="button" class="mod-item${m.id === activeId ? " active" : ""}" data-id="${escapeHtml(m.id)}">
|
for (const module of items) {
|
||||||
|
const bucket = byGroup.get(module.group);
|
||||||
|
if (bucket) bucket.push(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = groupsCache
|
||||||
|
.map((group) => {
|
||||||
|
const rows = byGroup.get(group.id) ?? [];
|
||||||
|
if (rows.length === 0) return "";
|
||||||
|
const buttons = rows
|
||||||
|
.map((module) => {
|
||||||
|
const tags = rowTags(module);
|
||||||
|
const tagHtml = tags.length
|
||||||
|
? `<span class="mod-item-tags">${tags
|
||||||
|
.map((tag) => `<span class="mod-tag">${escapeHtml(tag)}</span>`)
|
||||||
|
.join("")}</span>`
|
||||||
|
: "";
|
||||||
|
return `<button type="button" class="mod-item${module.id === activeId ? " active" : ""}" data-id="${escapeHtml(module.id)}">
|
||||||
<span class="mod-item-name">
|
<span class="mod-item-name">
|
||||||
<span>${escapeHtml(m.name)}</span>
|
<span>${escapeHtml(module.name)}</span>
|
||||||
<span class="mod-dot ${escapeHtml(m.status)}" title="${escapeHtml(STATUS_LABELS[m.status] ?? m.status)}"></span>
|
<span class="mod-dot ${escapeHtml(module.status)}" title="${escapeHtml(STATUS_LABELS[module.status] ?? module.status)}"></span>
|
||||||
</span>
|
</span>
|
||||||
<p class="mod-item-decl">${escapeHtml(m.declaration)}</p>
|
<p class="mod-item-decl">${escapeHtml(module.declaration)}</p>
|
||||||
</button>`,
|
${tagHtml}
|
||||||
)
|
</button>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
return `<section class="mod-group" aria-labelledby="grp-${escapeHtml(group.id)}">
|
||||||
|
<h2 class="mod-group-label" id="grp-${escapeHtml(group.id)}">${escapeHtml(group.label)}</h2>
|
||||||
|
<p class="mod-group-hint">${escapeHtml(group.hint)}</p>
|
||||||
|
${buttons}
|
||||||
|
</section>`;
|
||||||
|
})
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +143,10 @@ function metaField(meta, key) {
|
|||||||
return typeof v === "string" ? v.trim() : String(v);
|
return typeof v === "string" ? v.trim() : String(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupLabel(id) {
|
||||||
|
return groupsCache.find((group) => group.id === id)?.label ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
function renderDetail(detail) {
|
function renderDetail(detail) {
|
||||||
const title = document.getElementById("panel-title");
|
const title = document.getElementById("panel-title");
|
||||||
const sub = document.getElementById("panel-subtitle");
|
const sub = document.getElementById("panel-subtitle");
|
||||||
@@ -96,7 +155,8 @@ function renderDetail(detail) {
|
|||||||
const host = document.getElementById("mod-detail");
|
const host = document.getElementById("mod-detail");
|
||||||
|
|
||||||
title.textContent = detail.name;
|
title.textContent = detail.name;
|
||||||
sub.textContent = `${detail.id} · ${detail.artifact}`;
|
const group = groupLabel(detail.group);
|
||||||
|
sub.textContent = group ? `${group} · ${detail.artifact}` : detail.artifact;
|
||||||
|
|
||||||
badge.hidden = false;
|
badge.hidden = false;
|
||||||
badge.className = `mod-badge ${detail.status}`;
|
badge.className = `mod-badge ${detail.status}`;
|
||||||
@@ -108,10 +168,15 @@ function renderDetail(detail) {
|
|||||||
metaBar.hidden = !(when || whenNot || boundary);
|
metaBar.hidden = !(when || whenNot || boundary);
|
||||||
if (!metaBar.hidden) {
|
if (!metaBar.hidden) {
|
||||||
const bits = [];
|
const bits = [];
|
||||||
if (when) bits.push(`<strong>何时选用</strong> ${escapeHtml(when).replace(/\n/g, "<br>")}`);
|
if (when) bits.push(["何时选用", when]);
|
||||||
if (whenNot) bits.push(`<strong>何时不选</strong> ${escapeHtml(whenNot).replace(/\n/g, "<br>")}`);
|
if (whenNot) bits.push(["何时不选", whenNot]);
|
||||||
if (boundary) bits.push(`<strong>边界</strong> ${escapeHtml(boundary).replace(/\n/g, "<br>")}`);
|
if (boundary) bits.push(["边界", boundary]);
|
||||||
metaBar.innerHTML = bits.join("<hr style='border:none;border-top:1px solid var(--st-border);margin:10px 0'>");
|
metaBar.innerHTML = bits
|
||||||
|
.map(
|
||||||
|
([label, text]) =>
|
||||||
|
`<div class="mod-meta-row"><strong>${escapeHtml(label)}</strong><p>${escapeHtml(text).replace(/\n/g, "<br>")}</p></div>`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
const present = SECTION_ORDER.filter((id) => detail.sections?.[id]);
|
const present = SECTION_ORDER.filter((id) => detail.sections?.[id]);
|
||||||
@@ -135,12 +200,28 @@ function renderDetail(detail) {
|
|||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
|
const facts = [
|
||||||
|
["产物", `<code>${escapeHtml(detail.artifact)}</code>`],
|
||||||
|
["节点", `<code>${escapeHtml(detail.id)}</code>`],
|
||||||
|
];
|
||||||
|
if (LAYER_LABELS[detail.layer]) facts.push(["产物层", escapeHtml(LAYER_LABELS[detail.layer])]);
|
||||||
|
const tags = rowTags(detail);
|
||||||
|
if (detail.programSide) tags.push("程序编译");
|
||||||
|
if (tags.length) facts.push(["标记", escapeHtml(tags.join(" · "))]);
|
||||||
|
if (detail.libraries?.length) {
|
||||||
|
facts.push(["绑定库", escapeHtml(detail.libraries.join("、"))]);
|
||||||
|
}
|
||||||
|
if (detail.land) facts.push(["落盘", escapeHtml(detail.land)]);
|
||||||
|
facts.push([
|
||||||
|
"已有块",
|
||||||
|
escapeHtml(present.map((id) => SECTION_LABELS[id] ?? id).join(" · ") || "—"),
|
||||||
|
]);
|
||||||
|
|
||||||
host.innerHTML = `
|
host.innerHTML = `
|
||||||
<div class="mod-summary">
|
<div class="mod-summary">
|
||||||
<p class="mod-decl">${escapeHtml(detail.declaration)}</p>
|
<p class="mod-decl">${escapeHtml(detail.declaration)}</p>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>产物 tag</dt><dd><code>${escapeHtml(detail.artifact)}</code></dd>
|
${facts.map(([label, value]) => `<dt>${escapeHtml(label)}</dt><dd>${value}</dd>`).join("")}
|
||||||
<dt>已有块</dt><dd>${present.map((id) => SECTION_LABELS[id] ?? id).join(" · ") || "—"}</dd>
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
${nav}
|
${nav}
|
||||||
@@ -192,19 +273,22 @@ async function boot() {
|
|||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
modulesCache = data.modules || [];
|
modulesCache = data.modules || [];
|
||||||
document.getElementById("pack-label").textContent =
|
groupsCache = data.groups || [];
|
||||||
`配方包 · ${data.skillPackId || "—"} · ${modulesCache.length} 项`;
|
|
||||||
renderList();
|
renderList();
|
||||||
|
|
||||||
const fromHash = decodeURIComponent((location.hash || "").replace(/^#/, ""));
|
const fromHash = decodeURIComponent((location.hash || "").replace(/^#/, ""));
|
||||||
const initial =
|
const initial =
|
||||||
(fromHash && modulesCache.find((m) => m.id === fromHash)?.id) ||
|
(fromHash && modulesCache.find((m) => m.id === fromHash)?.id) ||
|
||||||
modulesCache.find((m) => m.status === "ready")?.id ||
|
modulesCache.find((m) => m.group === "content" && m.status === "ready")?.id ||
|
||||||
|
modulesCache.find((m) => m.group === "content")?.id ||
|
||||||
modulesCache[0]?.id;
|
modulesCache[0]?.id;
|
||||||
if (initial) await selectModule(initial);
|
if (initial) await selectModule(initial);
|
||||||
|
else document.getElementById("pack-label").textContent = "没有节点";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
document.getElementById("pack-label").textContent = "加载失败";
|
document.getElementById("pack-label").textContent = "加载失败";
|
||||||
|
document.getElementById("mod-list").innerHTML =
|
||||||
|
`<p class="mod-empty">节点目录没有加载出来</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+394
-66
@@ -692,91 +692,270 @@ body.play-workspace .coord-rail-title { display: none; }
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
.play-table {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
.play-table-list {
|
.play-table-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
}
|
||||||
.play-table-row {
|
.play-table-row {
|
||||||
display: grid;
|
border-bottom: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: start;
|
|
||||||
padding: 4px 0;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
}
|
||||||
.play-table-hit {
|
.play-table-row:hover {
|
||||||
display: flex;
|
background: color-mix(in srgb, var(--text) 3.5%, transparent);
|
||||||
flex-direction: column;
|
}
|
||||||
gap: 2px;
|
.play-table-row:focus-within {
|
||||||
align-items: flex-start;
|
background: color-mix(in srgb, var(--accent-soft) 70%, transparent);
|
||||||
|
}
|
||||||
|
.play-table-row.is-dirty {
|
||||||
|
box-shadow: inset 2px 0 0 var(--accent);
|
||||||
|
}
|
||||||
|
.play-table-row.is-invalid {
|
||||||
|
box-shadow: inset 2px 0 0 var(--danger);
|
||||||
|
}
|
||||||
|
.play-table-field,
|
||||||
|
.play-table-reveal {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(4.75rem, 46%);
|
||||||
|
gap: 2px 10px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 4px 6px 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.play-table-row.is-stack .play-table-field,
|
||||||
|
.play-table-row.is-wide .play-table-field {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.play-table-row.is-wide .play-table-input:not([type="checkbox"]) {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none;
|
||||||
|
text-align: left;
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
.play-table-reveal {
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0;
|
cursor: pointer;
|
||||||
cursor: default;
|
border-radius: 4px;
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
.play-table-row.is-secret .play-table-hit { cursor: pointer; }
|
.play-table-reveal:focus-visible {
|
||||||
.play-table-hit:focus-visible {
|
|
||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--accent);
|
||||||
outline-offset: 2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
.play-table-key {
|
.play-table-key {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.play-table-val {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text);
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.play-table-row.is-stack .play-table-val,
|
||||||
|
.play-table-val.is-code {
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.play-table-val.is-code {
|
||||||
|
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
padding: 2px 0 4px;
|
||||||
|
}
|
||||||
|
.play-table-nested,
|
||||||
|
.play-table-tree,
|
||||||
|
.play-table-json {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 2px 0 4px;
|
||||||
|
}
|
||||||
|
.play-table-json {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: color-mix(in srgb, var(--text) 5%, transparent);
|
||||||
|
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 220px;
|
||||||
|
}
|
||||||
|
.play-nested-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.play-nested-table th,
|
||||||
|
.play-nested-table td {
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border, var(--text)) 35%, transparent);
|
||||||
|
padding: 3px 6px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.play-nested-table th {
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
background: color-mix(in srgb, var(--text) 4%, transparent);
|
||||||
|
}
|
||||||
|
.play-table-tree {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 0 2px;
|
||||||
|
}
|
||||||
|
.play-nested-pair {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 3px 0 3px 8px;
|
||||||
|
border-left: 2px solid color-mix(in srgb, var(--text) 14%, transparent);
|
||||||
|
}
|
||||||
|
.play-nested-key {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.play-table-val {
|
.play-nested-leaf {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
word-break: break-word;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
.play-table-row.is-secret:not(.is-open) .play-table-val {
|
.play-table-row.is-secret:not(.is-open) .play-table-val {
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.14em;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.play-table-fix {
|
.play-table-input {
|
||||||
border: 0;
|
width: 100%;
|
||||||
background: transparent;
|
|
||||||
color: var(--muted);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.play-table-fix:hover { color: var(--text); }
|
|
||||||
.play-table-edit {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.play-table-edit input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: color-mix(in srgb, var(--text) 4%, transparent);
|
||||||
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 4px 6px;
|
line-height: 1.35;
|
||||||
border: 1px solid var(--border);
|
font-variant-numeric: tabular-nums;
|
||||||
border-radius: 4px;
|
text-align: right;
|
||||||
background: var(--bg);
|
padding: 3px 6px;
|
||||||
color: var(--text);
|
|
||||||
}
|
}
|
||||||
.play-table-secrets { margin-top: 10px; }
|
.play-table-row.is-inline .play-table-input:not([type="checkbox"]) {
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
.play-table-row.is-stack .play-table-input,
|
||||||
|
textarea.play-table-input {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
textarea.play-table-input {
|
||||||
|
display: block;
|
||||||
|
resize: none;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
min-height: 32px;
|
||||||
|
max-height: 148px;
|
||||||
|
}
|
||||||
|
textarea.play-table-input.is-code {
|
||||||
|
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.play-table-input:hover {
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
.play-table-input:focus {
|
||||||
|
outline: 2px solid var(--accent-soft);
|
||||||
|
outline-offset: 0;
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.play-table-input:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.play-table-input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 0 6px 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
justify-self: end;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.play-table-input[type="checkbox"]:focus {
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.play-table-row.is-dirty .play-table-input {
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.play-table-row.is-invalid .play-table-input {
|
||||||
|
border-color: color-mix(in srgb, var(--danger) 60%, var(--border));
|
||||||
|
}
|
||||||
|
.play-table-field-error {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.play-table-field-error[hidden] { display: none; }
|
||||||
|
.play-table-bar {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 8px -8px -8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.play-table-bar[hidden] { display: none; }
|
||||||
|
.play-table-bar-note {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.play-table-bar .btn-primary { flex: 0 0 auto; }
|
||||||
|
.play-table-secrets { margin-top: 8px; }
|
||||||
.play-table-secrets > summary {
|
.play-table-secrets > summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding: 4px 0;
|
padding: 6px 2px;
|
||||||
|
}
|
||||||
|
.play-table-secrets > summary:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
.play-open {
|
.play-open {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1254,11 +1433,13 @@ body.design-workspace[data-design-surface="answer"] .qcard-pager,
|
|||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 转述:对话在 stage 内的 .dictate-feed;#message-feed 保持隐藏 */
|
/* 转述 / 听书:对话在 stage 内的 .dictate-feed;#message-feed 保持隐藏 */
|
||||||
body.design-workspace[data-design-surface="dictate"] .message-feed {
|
body.design-workspace[data-design-surface="dictate"] .message-feed,
|
||||||
|
body.design-workspace[data-design-surface="listen"] .message-feed {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
body.design-workspace[data-design-surface="dictate"] .workspace-stage {
|
body.design-workspace[data-design-surface="dictate"] .workspace-stage,
|
||||||
|
body.design-workspace[data-design-surface="listen"] .workspace-stage {
|
||||||
order: 0;
|
order: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -1268,12 +1449,76 @@ body.design-workspace[data-design-surface="dictate"] .workspace-stage {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
body.design-workspace[data-design-surface="dictate"] {
|
body.design-workspace[data-design-surface="dictate"],
|
||||||
|
body.design-workspace[data-design-surface="listen"] {
|
||||||
--coord-w: clamp(300px, 28vw, 380px);
|
--coord-w: clamp(300px, 28vw, 380px);
|
||||||
}
|
}
|
||||||
body.design-workspace[data-design-surface="dictate"] .panel-feed {
|
body.design-workspace[data-design-surface="dictate"] .panel-feed,
|
||||||
|
body.design-workspace[data-design-surface="listen"] .panel-feed {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.listen-rail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.listen-meta {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.listen-brief {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.listen-outline {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.1em;
|
||||||
|
}
|
||||||
|
.listen-outline-item {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
.listen-outline-item.status-cut {
|
||||||
|
opacity: 0.45;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.listen-sections {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.listen-section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.listen-section-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.listen-section.is-playing {
|
||||||
|
outline: 1px solid color-mix(in srgb, var(--accent, #6af) 55%, transparent);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
.listen-section-body {
|
||||||
|
white-space: normal;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.listen-rail-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
padding-top: 8px;
|
||||||
|
background: linear-gradient(transparent, var(--panel, var(--bg)) 40%);
|
||||||
|
}
|
||||||
.dictate-stage-bar {
|
.dictate-stage-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
@@ -1295,6 +1540,7 @@ body.design-workspace[data-design-surface="dictate"] .panel-feed {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-anchor: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -2274,7 +2520,8 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.explorer-row.has-twist .explorer-main { grid-column: 2; }
|
.explorer-row.has-twist .explorer-main { grid-column: 2; }
|
||||||
.explorer-row.has-twist .explorer-del { grid-column: 3; }
|
.explorer-row.has-twist .explorer-del,
|
||||||
|
.explorer-row.has-twist .explorer-actions { grid-column: 3; }
|
||||||
.explorer-row.explorer-process {
|
.explorer-row.explorer-process {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
@@ -2341,6 +2588,19 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
|||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
.explorer-actions {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
align-self: center;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.explorer-actions .explorer-del,
|
||||||
|
.explorer-actions .explorer-add {
|
||||||
|
grid-column: auto;
|
||||||
|
grid-row: auto;
|
||||||
|
}
|
||||||
.explorer-del {
|
.explorer-del {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1 / span 2;
|
grid-row: 1 / span 2;
|
||||||
@@ -2370,20 +2630,19 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 28px;
|
width: 22px;
|
||||||
height: 28px;
|
height: 22px;
|
||||||
margin: 0 0 2px 8px;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.explorer-add.child { margin-left: 28px; }
|
|
||||||
.explorer-add:hover {
|
.explorer-add:hover {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@@ -2495,6 +2754,7 @@ body.design-workspace .flow-step[data-run="done"] .flow-name {
|
|||||||
.message-feed {
|
.message-feed {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-anchor: none;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2725,6 +2985,18 @@ body.rail-collapsed .message-feed {
|
|||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
body[data-lifecycle="play"] .msg-play-aux .msg-bubble-aux {
|
||||||
|
border-style: dashed;
|
||||||
|
opacity: 0.96;
|
||||||
|
}
|
||||||
|
body[data-lifecycle="play"] .msg-play-aux .msg-tag {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
body[data-lifecycle="play"] .msg-play-aux .msg-body {
|
||||||
|
font-size: 0.92em;
|
||||||
|
max-height: min(42vh, 28rem);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.msg-live-indicator {
|
.msg-live-indicator {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -3912,8 +4184,9 @@ body.design-workspace .af-mosaic {
|
|||||||
.present-preview-frame {
|
.present-preview-frame {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 220px;
|
height: 1px;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
overflow: hidden;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
.present-preview-fallback {
|
.present-preview-fallback {
|
||||||
@@ -5712,6 +5985,18 @@ body.design-workspace .af-mosaic {
|
|||||||
.msg-peek-body .dictate-product-body {
|
.msg-peek-body .dictate-product-body {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
.msg-raw-section {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
.msg-raw-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.msg-raw-kicker {
|
||||||
|
margin: 0 0 0.4rem;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--muted, #6b7280);
|
||||||
|
}
|
||||||
.msg-peek-body .artifact-compact-body {
|
.msg-peek-body .artifact-compact-body {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
@@ -6717,14 +7002,57 @@ menu.context-menu[hidden],
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
max-height: min(84vh, 860px);
|
max-height: min(84vh, 860px);
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.context-trace-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
.context-trace-kicker {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.context-trace-system {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--muted, #888) 9%, transparent);
|
||||||
|
}
|
||||||
|
.context-trace-system .dialog-desc {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#context-trace-messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
#context-trace-messages[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.context-trace-outline {
|
||||||
|
min-height: 0;
|
||||||
|
max-height: none;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
.context-trace-body {
|
.context-trace-body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
overflow: auto;
|
overflow: visible;
|
||||||
flex: 1;
|
flex: none;
|
||||||
min-height: 240px;
|
min-height: 0;
|
||||||
max-height: min(60vh, 640px);
|
max-height: none;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
Reference in New Issue
Block a user