feat(cli): 章節與寫作命令(fox chapters/fox generate,issue #43)
- fox chapters list/get/create/update/delete:對接 #40 章節 CRUD 端點; 公開命令(list/get)在持有 token 時自動帶上(作者本人可見 draft/hidden)。 - fox generate draft/continue/rewrite/usages:對接 #42 生成 API; 需 generate scope 的 access token。 - 補齊 M3:fox novels list/create/update(Bearer 授權)+ tokens create --scopes。 - --content/--prompt/--previous-context 支援 '-' 從 stdin 讀多行內容。 - 輸出維持單一 JSON(stdout)、exit code 0/1/2 規範不變。 - cli/README.md:命令表、授權說明與 M3 里程碑更新。 本機後端(Postgres+NestJS,echo 供應商)端到端實測 44 項全過。
This commit is contained in:
+36
-5
@@ -12,7 +12,13 @@
|
||||
import { loadConfig } from "./config.js";
|
||||
import { ApiError, UsageError } from "./errors.js";
|
||||
import { parseArgs, Registry } from "./command.js";
|
||||
import { registerAuthCommands, registerNovelCommands, registerTokenCommands } from "./commands.js";
|
||||
import {
|
||||
registerAuthCommands,
|
||||
registerChapterCommands,
|
||||
registerGenerateCommands,
|
||||
registerNovelCommands,
|
||||
registerTokenCommands,
|
||||
} from "./commands.js";
|
||||
|
||||
const PROGRAM = "fox";
|
||||
|
||||
@@ -26,10 +32,30 @@ exit code:0 成功;1 執行失敗(後端 4xx/5xx、網路錯誤);2 用
|
||||
fox auth status 查詢登入/SSO 設定(公開)
|
||||
fox tokens verify 驗證手中 token 是否有效
|
||||
fox tokens create --name <名稱> 建立新 token(原始值只顯示一次)
|
||||
[--expires-at <ISO8601>]
|
||||
[--expires-at <ISO8601>] [--scopes read,write,generate]
|
||||
fox tokens list 列出 token(不含 hash/原始值)
|
||||
fox tokens revoke <id> 撤銷 token(冪等)
|
||||
fox novels get <slug> 查單一作品(公開)
|
||||
fox novels list 列出自己的作品(需授權)
|
||||
fox novels create --title <書名> 建立作品(需授權)
|
||||
[--slug <slug>] [--synopsis <簡介>] [--tags <標籤1,標籤2>]
|
||||
fox novels update <slug> […] 更新作品(需授權;見 fox novels update --help)
|
||||
fox chapters list <novelSlug> 列章節(公開僅 published;作者 token 見全部)
|
||||
fox chapters get <novelSlug> <chapterSlug>
|
||||
查單一章節(draft 僅作者/admin)
|
||||
fox chapters create <novelSlug> --title <標題>
|
||||
建立章節(需授權;狀態預設 draft)
|
||||
fox chapters update <novelSlug> <chapterSlug> […]
|
||||
更新章節(需授權)
|
||||
fox chapters delete <novelSlug> <chapterSlug>
|
||||
刪除章節(需授權;冪等)
|
||||
fox generate draft <novelSlug> --prompt <指示>
|
||||
草稿生成(需 generate scope)
|
||||
fox generate continue <novelSlug> --previous-context <前文摘要>
|
||||
續寫(前文摘要必填)
|
||||
fox generate rewrite <novelSlug> --prompt <原文>
|
||||
改寫(原文放在 --prompt)
|
||||
fox generate usages <novelSlug> 查詢生成用量記帳(需授權)
|
||||
|
||||
全域選項(放在子命令後):
|
||||
--api-url <URL> 後端 API 位址(預設 FOX_API_URL 或 http://localhost:3000)
|
||||
@@ -39,16 +65,21 @@ exit code:0 成功;1 執行失敗(後端 4xx/5xx、網路錯誤);2 用
|
||||
設定優先順序:--token/--api-url > FOX_API_TOKEN/FOX_API_URL >
|
||||
~/.config/fox/cli.json({"apiUrl": "...", "token": "..."},權限建議 600)。
|
||||
|
||||
尚未支援(見 cli/README.md 授權缺口):novels list/create/update —
|
||||
後端 novels 授權端點目前只吃瀏覽器 session,待後端接受 Bearer access token 後補齊。
|
||||
內容類選項(--content/--prompt/--previous-context)值為 "-" 時改從 stdin
|
||||
讀入(適合多行文字:cat 章節.md | fox chapters create <slug> --title X --content -)。
|
||||
|
||||
每個命令可用 --help 查看詳細說明,例如:fox tokens create --help`;
|
||||
fox generate 命令需要具備 generate scope 的 access token
|
||||
(建立時 --scopes read,write,generate;缺 scope 會回 403)。
|
||||
|
||||
每個命令可用 --help 查看詳細說明,例如:fox chapters create --help`;
|
||||
|
||||
function buildRegistry(): Registry {
|
||||
const registry = new Registry();
|
||||
registerAuthCommands(registry);
|
||||
registerTokenCommands(registry);
|
||||
registerNovelCommands(registry);
|
||||
registerChapterCommands(registry);
|
||||
registerGenerateCommands(registry);
|
||||
return registry;
|
||||
}
|
||||
|
||||
|
||||
+367
-5
@@ -20,6 +20,55 @@ async function call(
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 讀取內容類選項(--content/--prompt/--previous-context)的值。
|
||||
* 值為 "-" 時改從 stdin 讀入全部內容(適合多行文字經管線傳入);
|
||||
* stdin 是終端機時直接報用法錯誤,避免掛起等待輸入。
|
||||
*/
|
||||
async function readValue(
|
||||
flagName: string,
|
||||
value: string | undefined,
|
||||
): Promise<string | undefined> {
|
||||
if (value !== "-") return value;
|
||||
if (process.stdin.isTTY) {
|
||||
throw new UsageError(
|
||||
`--${flagName} - 需經管線傳入內容(stdin 是終端機,會無限等待);用法:cat 檔案 | fox … --${flagName} -`,
|
||||
);
|
||||
}
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk as Buffer);
|
||||
}
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
|
||||
/** 讀取位置參數並要求非空。 */
|
||||
function requirePositional(
|
||||
positionals: string[],
|
||||
index: number,
|
||||
name: string,
|
||||
usage: string,
|
||||
): string {
|
||||
const value = positionals[index];
|
||||
if (value === undefined || !value.trim()) {
|
||||
throw new UsageError(`缺少 <${name}>;用法:${usage}`);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
/** 讀取必填的字串選項。 */
|
||||
async function requireFlag(
|
||||
flags: Record<string, string | boolean>,
|
||||
flagName: string,
|
||||
usage: string,
|
||||
): Promise<string> {
|
||||
const value = await readValue(flagName, flagString(flags, flagName));
|
||||
if (value === undefined || !value.trim()) {
|
||||
throw new UsageError(`缺少 --${flagName};用法:${usage}`);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
export function registerAuthCommands(registry: Registry): void {
|
||||
registry.add({
|
||||
path: ["auth", "status"],
|
||||
@@ -44,18 +93,37 @@ export function registerTokenCommands(registry: Registry): void {
|
||||
registry.add({
|
||||
path: ["tokens", "create"],
|
||||
summary: "建立新 token(原始值只在這次輸出顯示)",
|
||||
usage: "fox tokens create --name <名稱> [--expires-at <ISO8601>]",
|
||||
usage:
|
||||
"fox tokens create --name <名稱> [--expires-at <ISO8601>] [--scopes read,write,generate]",
|
||||
run: async (ctx) => {
|
||||
const name = flagString(ctx.args.flags, "name");
|
||||
if (!name || !name.trim()) {
|
||||
throw new UsageError("缺少 --name <名稱>;用法:fox tokens create --name <名稱> [--expires-at <ISO8601>]");
|
||||
throw new UsageError(
|
||||
"缺少 --name <名稱>;用法:fox tokens create --name <名稱> [--expires-at <ISO8601>] [--scopes read,write,generate]",
|
||||
);
|
||||
}
|
||||
const expiresAt = flagString(ctx.args.flags, "expires-at");
|
||||
if (expiresAt !== undefined && Number.isNaN(Date.parse(expiresAt))) {
|
||||
throw new UsageError(`--expires-at 不是有效的 ISO 8601 日期:${expiresAt}`);
|
||||
}
|
||||
const body: Record<string, string> = { name: name.trim() };
|
||||
const body: Record<string, unknown> = { name: name.trim() };
|
||||
if (expiresAt !== undefined) body.expiresAt = new Date(expiresAt).toISOString();
|
||||
|
||||
const scopes = flagString(ctx.args.flags, "scopes");
|
||||
if (scopes !== undefined) {
|
||||
const list = scopes
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const valid = ["read", "write", "generate"];
|
||||
const bad = list.filter((s) => !valid.includes(s));
|
||||
if (bad.length > 0) {
|
||||
throw new UsageError(
|
||||
`--scopes 含無效值:${bad.join(",")}(可用:read、write、generate)`,
|
||||
);
|
||||
}
|
||||
body.scopes = list;
|
||||
}
|
||||
await call(ctx, "access-tokens", { method: "POST", body });
|
||||
},
|
||||
});
|
||||
@@ -85,17 +153,311 @@ export function registerTokenCommands(registry: Registry): void {
|
||||
});
|
||||
}
|
||||
|
||||
const CHAPTER_USAGE =
|
||||
"fox chapters <list|get|create|update|delete> <novelSlug> …(見 fox chapters --help)";
|
||||
const CHAPTER_STATUSES = ["draft", "published", "hidden"] as const;
|
||||
const NOVEL_STATUSES = ["draft", "serializing", "completed", "hidden"] as const;
|
||||
|
||||
/** 讀取 --status 並驗證值域(draft/published/hidden)。 */
|
||||
function readChapterStatusFlag(flags: Record<string, string | boolean>): string | undefined {
|
||||
const value = flagString(flags, "status");
|
||||
if (value === undefined) return undefined;
|
||||
if (!CHAPTER_STATUSES.includes(value as (typeof CHAPTER_STATUSES)[number])) {
|
||||
throw new UsageError(`--status 不正確(${CHAPTER_STATUSES.join("|")}):${value}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 讀取 --status 並驗證值域(draft/serializing/completed/hidden)。 */
|
||||
function readNovelStatusFlag(flags: Record<string, string | boolean>): string | undefined {
|
||||
const value = flagString(flags, "status");
|
||||
if (value === undefined) return undefined;
|
||||
if (!NOVEL_STATUSES.includes(value as (typeof NOVEL_STATUSES)[number])) {
|
||||
throw new UsageError(`--status 不正確(${NOVEL_STATUSES.join("|")}):${value}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 讀取 --sort-order 並驗證為 0–1000000 的整數。 */
|
||||
function readSortOrderFlag(flags: Record<string, string | boolean>): number | undefined {
|
||||
const raw = flagString(flags, "sort-order");
|
||||
if (raw === undefined) return undefined;
|
||||
const n = Number(raw);
|
||||
if (!/^\d+$/.test(raw.trim()) || !Number.isInteger(n) || n < 0 || n > 1_000_000) {
|
||||
throw new UsageError(`--sort-order 須為 0–1000000 的整數:${raw}`);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
export function registerNovelCommands(registry: Registry): void {
|
||||
registry.add({
|
||||
path: ["novels", "get"],
|
||||
summary: "查單一作品(公開端點)",
|
||||
usage: "fox novels get <slug>",
|
||||
run: async (ctx) => {
|
||||
const slug = ctx.args.positionals[0];
|
||||
if (!slug) throw new UsageError("缺少 <slug>;用法:fox novels get <slug>");
|
||||
const slug = requirePositional(ctx.args.positionals, 0, "slug", "fox novels get <slug>");
|
||||
await call(ctx, `novels/${encodeURIComponent(slug)}`);
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["novels", "list"],
|
||||
summary: "列出自己的作品(需授權)",
|
||||
usage: "fox novels list",
|
||||
run: async (ctx) => {
|
||||
await call(ctx, "novels", { auth: true });
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["novels", "create"],
|
||||
summary: "建立作品(需授權;slug 由書名自動生成,衝突時加流水號)",
|
||||
usage:
|
||||
"fox novels create --title <書名> [--slug <slug>] [--synopsis <簡介>] [--tags <標籤1,標籤2>]",
|
||||
run: async (ctx) => {
|
||||
const usage =
|
||||
"fox novels create --title <書名> [--slug <slug>] [--synopsis <簡介>] [--tags <標籤1,標籤2>]";
|
||||
const title = await requireFlag(ctx.args.flags, "title", usage);
|
||||
const body: Record<string, unknown> = { title };
|
||||
const slug = flagString(ctx.args.flags, "slug");
|
||||
if (slug !== undefined) body.slug = slug.trim();
|
||||
const synopsis = flagString(ctx.args.flags, "synopsis");
|
||||
if (synopsis !== undefined) body.synopsis = synopsis;
|
||||
const tags = flagString(ctx.args.flags, "tags");
|
||||
if (tags !== undefined) body.tags = tags;
|
||||
await call(ctx, "novels", { method: "POST", body, auth: true });
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["novels", "update"],
|
||||
summary: "更新作品(需授權;只傳要改的欄位)",
|
||||
usage:
|
||||
"fox novels update <slug> [--title <書名>] [--slug <新slug>] [--synopsis <簡介>] [--tags <標籤>] [--status <狀態>]",
|
||||
run: async (ctx) => {
|
||||
const usage =
|
||||
"fox novels update <slug> [--title <書名>] [--slug <新slug>] [--synopsis <簡介>] [--tags <標籤>] [--status <狀態>]";
|
||||
const slug = requirePositional(ctx.args.positionals, 0, "slug", usage);
|
||||
const body: Record<string, unknown> = {};
|
||||
const title = flagString(ctx.args.flags, "title");
|
||||
if (title !== undefined) body.title = title.trim();
|
||||
const newSlug = flagString(ctx.args.flags, "slug");
|
||||
if (newSlug !== undefined) body.slug = newSlug.trim();
|
||||
const synopsis = flagString(ctx.args.flags, "synopsis");
|
||||
if (synopsis !== undefined) body.synopsis = synopsis;
|
||||
const tags = flagString(ctx.args.flags, "tags");
|
||||
if (tags !== undefined) body.tags = tags;
|
||||
const status = readNovelStatusFlag(ctx.args.flags);
|
||||
if (status !== undefined) body.status = status;
|
||||
if (Object.keys(body).length === 0) {
|
||||
throw new UsageError(`沒有指定任何要更新的欄位;用法:${usage}`);
|
||||
}
|
||||
await call(ctx, `novels/${encodeURIComponent(slug)}`, {
|
||||
method: "PATCH",
|
||||
body,
|
||||
auth: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerChapterCommands(registry: Registry): void {
|
||||
registry.add({
|
||||
path: ["chapters", "list"],
|
||||
summary: "列章節(公開僅 published;帶作者 token 可見全部)",
|
||||
usage: "fox chapters list <novelSlug>",
|
||||
run: async (ctx) => {
|
||||
const slug = requirePositional(ctx.args.positionals, 0, "novelSlug", "fox chapters list <novelSlug>");
|
||||
// 公開端點:有 token 就帶上(作者本人可見 draft/hidden),沒有也照常呼叫。
|
||||
const config = await loadConfig({
|
||||
apiUrl: flagString(ctx.args.flags, "api-url"),
|
||||
token: flagString(ctx.args.flags, "token"),
|
||||
});
|
||||
const data = await request(
|
||||
config,
|
||||
`novels/${encodeURIComponent(slug)}/chapters`,
|
||||
config.token ? { auth: true } : {},
|
||||
);
|
||||
ctx.print(data);
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["chapters", "get"],
|
||||
summary: "查單一章節(draft 僅作者/admin)",
|
||||
usage: "fox chapters get <novelSlug> <chapterSlug>",
|
||||
run: async (ctx) => {
|
||||
const usage = "fox chapters get <novelSlug> <chapterSlug>";
|
||||
const novelSlug = requirePositional(ctx.args.positionals, 0, "novelSlug", usage);
|
||||
const chapterSlug = requirePositional(ctx.args.positionals, 1, "chapterSlug", usage);
|
||||
const config = await loadConfig({
|
||||
apiUrl: flagString(ctx.args.flags, "api-url"),
|
||||
token: flagString(ctx.args.flags, "token"),
|
||||
});
|
||||
const data = await request(
|
||||
config,
|
||||
`novels/${encodeURIComponent(novelSlug)}/chapters/${encodeURIComponent(chapterSlug)}`,
|
||||
config.token ? { auth: true } : {},
|
||||
);
|
||||
ctx.print(data);
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["chapters", "create"],
|
||||
summary: "建立章節(需授權;slug 由標題生成,狀態預設 draft)",
|
||||
usage:
|
||||
"fox chapters create <novelSlug> --title <標題> [--slug <slug>] [--content <內容|->] [--sort-order <N>] [--status <狀態>]",
|
||||
run: async (ctx) => {
|
||||
const usage =
|
||||
"fox chapters create <novelSlug> --title <標題> [--slug <slug>] [--content <內容|->] [--sort-order <N>] [--status <狀態>]";
|
||||
const novelSlug = requirePositional(ctx.args.positionals, 0, "novelSlug", usage);
|
||||
const title = await requireFlag(ctx.args.flags, "title", usage);
|
||||
const body: Record<string, unknown> = { title };
|
||||
const slug = flagString(ctx.args.flags, "slug");
|
||||
if (slug !== undefined) body.slug = slug.trim();
|
||||
const content = await readValue("content", flagString(ctx.args.flags, "content"));
|
||||
if (content !== undefined) body.content = content;
|
||||
const sortOrder = readSortOrderFlag(ctx.args.flags);
|
||||
if (sortOrder !== undefined) body.sortOrder = sortOrder;
|
||||
const status = readChapterStatusFlag(ctx.args.flags);
|
||||
if (status !== undefined) body.status = status;
|
||||
await call(ctx, `novels/${encodeURIComponent(novelSlug)}/chapters`, {
|
||||
method: "POST",
|
||||
body,
|
||||
auth: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["chapters", "update"],
|
||||
summary: "更新章節(需授權;只傳要改的欄位)",
|
||||
usage:
|
||||
"fox chapters update <novelSlug> <chapterSlug> [--title <標題>] [--slug <slug>] [--content <內容|->] [--sort-order <N>] [--status <狀態>]",
|
||||
run: async (ctx) => {
|
||||
const usage =
|
||||
"fox chapters update <novelSlug> <chapterSlug> [--title <標題>] [--slug <slug>] [--content <內容|->] [--sort-order <N>] [--status <狀態>]";
|
||||
const novelSlug = requirePositional(ctx.args.positionals, 0, "novelSlug", usage);
|
||||
const chapterSlug = requirePositional(ctx.args.positionals, 1, "chapterSlug", usage);
|
||||
const body: Record<string, unknown> = {};
|
||||
const title = flagString(ctx.args.flags, "title");
|
||||
if (title !== undefined) body.title = title.trim();
|
||||
const slug = flagString(ctx.args.flags, "slug");
|
||||
if (slug !== undefined) body.slug = slug.trim();
|
||||
const content = await readValue("content", flagString(ctx.args.flags, "content"));
|
||||
if (content !== undefined) body.content = content;
|
||||
const sortOrder = readSortOrderFlag(ctx.args.flags);
|
||||
if (sortOrder !== undefined) body.sortOrder = sortOrder;
|
||||
const status = readChapterStatusFlag(ctx.args.flags);
|
||||
if (status !== undefined) body.status = status;
|
||||
if (Object.keys(body).length === 0) {
|
||||
throw new UsageError(`沒有指定任何要更新的欄位;用法:${usage}`);
|
||||
}
|
||||
await call(
|
||||
ctx,
|
||||
`novels/${encodeURIComponent(novelSlug)}/chapters/${encodeURIComponent(chapterSlug)}`,
|
||||
{ method: "PATCH", body, auth: true },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["chapters", "delete"],
|
||||
summary: "刪除章節(需授權;冪等)",
|
||||
usage: "fox chapters delete <novelSlug> <chapterSlug>",
|
||||
run: async (ctx) => {
|
||||
const usage = "fox chapters delete <novelSlug> <chapterSlug>";
|
||||
const novelSlug = requirePositional(ctx.args.positionals, 0, "novelSlug", usage);
|
||||
const chapterSlug = requirePositional(ctx.args.positionals, 1, "chapterSlug", usage);
|
||||
await call(
|
||||
ctx,
|
||||
`novels/${encodeURIComponent(novelSlug)}/chapters/${encodeURIComponent(chapterSlug)}`,
|
||||
{ method: "DELETE", auth: true },
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerGenerateCommands(registry: Registry): void {
|
||||
/** 共用:組生成請求 body 並 POST。 */
|
||||
async function postGenerate(
|
||||
ctx: CommandContext,
|
||||
novelSlug: string,
|
||||
action: "draft" | "continue" | "rewrite",
|
||||
): Promise<void> {
|
||||
const usage = `fox generate ${action} <novelSlug> [--prompt <指示|->] [--previous-context <前文摘要|->]`;
|
||||
const body: Record<string, unknown> = {};
|
||||
const prompt = await readValue("prompt", flagString(ctx.args.flags, "prompt"));
|
||||
if (prompt !== undefined && prompt.trim()) body.prompt = prompt.trim();
|
||||
const previousContext = await readValue(
|
||||
"previous-context",
|
||||
flagString(ctx.args.flags, "previous-context"),
|
||||
);
|
||||
if (previousContext !== undefined && previousContext.trim()) {
|
||||
body.previousContext = previousContext.trim();
|
||||
}
|
||||
if (action === "draft" && body.prompt === undefined) {
|
||||
throw new UsageError(`缺少 --prompt <指示>;用法:${usage}`);
|
||||
}
|
||||
if (action === "continue" && body.previousContext === undefined) {
|
||||
throw new UsageError(`續寫需提供前文摘要;用法:${usage}`);
|
||||
}
|
||||
await call(ctx, `novels/${encodeURIComponent(novelSlug)}/generate${action === "draft" ? "" : `/${action}`}`, {
|
||||
method: "POST",
|
||||
body,
|
||||
auth: true,
|
||||
});
|
||||
}
|
||||
|
||||
registry.add({
|
||||
path: ["generate", "draft"],
|
||||
summary: "草稿生成(需授權;token 需 generate scope)",
|
||||
usage: "fox generate draft <novelSlug> --prompt <指示|-> [--previous-context <前文摘要|->]",
|
||||
run: async (ctx) => {
|
||||
const usage =
|
||||
"fox generate draft <novelSlug> --prompt <指示|-> [--previous-context <前文摘要|->]";
|
||||
const novelSlug = requirePositional(ctx.args.positionals, 0, "novelSlug", usage);
|
||||
await postGenerate(ctx, novelSlug, "draft");
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["generate", "continue"],
|
||||
summary: "續寫(需授權;--previous-context 必填)",
|
||||
usage: "fox generate continue <novelSlug> --previous-context <前文摘要|-> [--prompt <指示|->]",
|
||||
run: async (ctx) => {
|
||||
const usage =
|
||||
"fox generate continue <novelSlug> --previous-context <前文摘要|-> [--prompt <指示|->]";
|
||||
const novelSlug = requirePositional(ctx.args.positionals, 0, "novelSlug", usage);
|
||||
await postGenerate(ctx, novelSlug, "continue");
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["generate", "rewrite"],
|
||||
summary: "改寫(需授權;原文放在 --prompt)",
|
||||
usage: "fox generate rewrite <novelSlug> --prompt <原文|-> [--previous-context <前文摘要|->]",
|
||||
run: async (ctx) => {
|
||||
const usage =
|
||||
"fox generate rewrite <novelSlug> --prompt <原文|-> [--previous-context <前文摘要|->]";
|
||||
const novelSlug = requirePositional(ctx.args.positionals, 0, "novelSlug", usage);
|
||||
await postGenerate(ctx, novelSlug, "rewrite");
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["generate", "usages"],
|
||||
summary: "查詢生成用量記帳(需授權;作者/admin)",
|
||||
usage: "fox generate usages <novelSlug>",
|
||||
run: async (ctx) => {
|
||||
const usage = "fox generate usages <novelSlug>";
|
||||
const novelSlug = requirePositional(ctx.args.positionals, 0, "novelSlug", usage);
|
||||
await call(ctx, `novels/${encodeURIComponent(novelSlug)}/generate/usages`, {
|
||||
auth: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 共用型別:命令實作可引用的介面(保持單一來源)。 */
|
||||
|
||||
Reference in New Issue
Block a user