Просмотр исходного кода

fix(v13): accept both locale role paths in studio and write_truth_file [phase hotfix 3]

The runtime reader (utils/outline-paths.ts) already scans both Chinese
(roles/主要角色, roles/次要角色) and English (roles/major, roles/minor) tier
dirs. Studio and the agent's write_truth_file whitelist accepted only the
Chinese variants — English-layout books were readable by the runtime but
invisible in Studio's truth browser, unreachable through the truth GET/PUT
endpoints, and unwritable by the agent.

Fix:
- studio/src/api/server.ts resolveTruthFilePath: extend role regex to
  /(主要角色|次要角色|major|minor)/.
- studio truth-browser listing: enumerate roles/major and roles/minor in
  addition to the existing zh dirs.
- core/src/pipeline/agent.ts ROLE_PATH_PATTERN: extend to match en locale
  and update the error-message examples accordingly.

Path traversal safety preserved: the regex still bounds [^/]+\.md to
single-file entries one level deep under a known tier dir.
Ma 6 месяцев назад
Родитель
Сommit
ba23b640ec
2 измененных файлов с 26 добавлено и 7 удалено
  1. 6 1
      packages/core/src/pipeline/agent.ts
  2. 20 6
      packages/studio/src/api/server.ts

+ 6 - 1
packages/core/src/pipeline/agent.ts

@@ -578,7 +578,10 @@ export async function executeAgentTool(
         "outline/story_frame.md", "outline/volume_map.md",
         "outline/节奏原则.md", "outline/rhythm_principles.md",
       ];
-      const ROLE_PATH_PATTERN = /^roles\/(主要角色|次要角色)\/[^/]+\.md$/;
+      // Phase hotfix 3: accept both locale dirs so English-layout books can
+      // be edited via write_truth_file. The reader (utils/outline-paths.ts)
+      // and Studio (server.ts) accept both — the agent whitelist must match.
+      const ROLE_PATH_PATTERN = /^roles\/(主要角色|次要角色|major|minor)\/[^/]+\.md$/;
 
       const isAllowed =
         ALLOWED_FLAT_FILES.includes(fileName)
@@ -591,6 +594,8 @@ export async function executeAgentTool(
           ...ALLOWED_OUTLINE_FILES,
           "roles/主要角色/<name>.md",
           "roles/次要角色/<name>.md",
+          "roles/major/<name>.md",
+          "roles/minor/<name>.md",
         ];
         return JSON.stringify({
           error:

+ 20 - 6
packages/studio/src/api/server.ts

@@ -290,10 +290,14 @@ export function createStudioServer(initialConfig: ProjectConfig, root: string) {
       return null;
     }
 
+    // Phase hotfix 3: accept both Chinese and English locale role dirs so
+    // English-layout books (roles/major, roles/minor) are reachable through
+    // Studio. The runtime reader (utils/outline-paths.ts:75) already scans
+    // both — Studio used to drop English books to read-only.
     const allowed =
       TRUTH_FLAT_FILES.includes(file)
       || TRUTH_OUTLINE_FILES.includes(file)
-      || /^roles\/(主要角色|次要角色)\/[^/]+\.md$/.test(file);
+      || /^roles\/(主要角色|次要角色|major|minor)\/[^/]+\.md$/.test(file);
 
     if (!allowed) return null;
 
@@ -535,11 +539,21 @@ export function createStudioServer(initialConfig: ProjectConfig, root: string) {
       const flatFiles = (await listDir(".")).filter((f) => !f.startsWith("outline") && !f.startsWith("roles"));
       // Phase 5 outline/ files
       const outlineFiles = (await listDir("outline")).map((f) => `outline/${f}`);
-      // Phase 5 roles/主要角色 + roles/次要角色
-      const majorRoles = (await listDir("roles/主要角色")).map((f) => `roles/主要角色/${f}`);
-      const minorRoles = (await listDir("roles/次要角色")).map((f) => `roles/次要角色/${f}`);
-
-      const all = [...flatFiles, ...outlineFiles, ...majorRoles, ...minorRoles];
+      // Phase 5 roles/主要角色 + roles/次要角色, plus Phase hotfix 3
+      // English-locale equivalents so en-language books are visible.
+      const majorRolesZh = (await listDir("roles/主要角色")).map((f) => `roles/主要角色/${f}`);
+      const minorRolesZh = (await listDir("roles/次要角色")).map((f) => `roles/次要角色/${f}`);
+      const majorRolesEn = (await listDir("roles/major")).map((f) => `roles/major/${f}`);
+      const minorRolesEn = (await listDir("roles/minor")).map((f) => `roles/minor/${f}`);
+
+      const all = [
+        ...flatFiles,
+        ...outlineFiles,
+        ...majorRolesZh,
+        ...minorRolesZh,
+        ...majorRolesEn,
+        ...minorRolesEn,
+      ];
       const described = await Promise.all(all.map(describe));
       const result = described.filter((x): x is NonNullable<typeof x> => x !== null);
       return c.json({ files: result });