@waterwx/dsh-novel-forge 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,6 +62,10 @@ export declare class NovelApi {
62
62
  biblePatch(patch: import('../protocol.ts').BiblePatchRequest): Promise<{
63
63
  bible: import('../protocol.ts').StoryBible;
64
64
  }>;
65
+ /** 剧情线管理:增删改 + 关联章节。 */
66
+ plotlines(req: import('../protocol.ts').PlotlinesRequest): Promise<import('../protocol.ts').PlotlinesResponse>;
67
+ /** 敏感词检查:指定章节 / 任意文本 / 全书。 */
68
+ sensitiveCheck(req: import('../protocol.ts').SensitiveCheckRequest): Promise<import('../protocol.ts').SensitiveCheckResponse>;
65
69
  /** 小说简介:AI 生成/补全(partial 留空 = 全量),或手动保存。 */
66
70
  blurb(action: 'generate' | 'save', text?: string, partial?: string): Promise<{
67
71
  blurb: string;
@@ -99,11 +99,53 @@ export declare const zh: {
99
99
  readonly 'foreshadow.plantedAt': "埋设于";
100
100
  readonly 'foreshadow.setPlanted': "标记已埋设";
101
101
  readonly 'foreshadow.setResolved': "标记已回收";
102
+ readonly 'tab.facts': "编年录";
103
+ readonly 'tab.plotlines': "剧情线";
104
+ readonly 'plotlines.hint': "主线/支线/人物线/悬念线的推进管理:章节生成时会注入未完结的线,要求每章至少推进一条;关联章节记录推进节点。";
105
+ readonly 'plotlines.empty': "还没有剧情线——长篇小说建议先建主线与几条支线。";
106
+ readonly 'plotlines.new': "+ 新建剧情线";
107
+ readonly 'plotlines.name': "线名";
108
+ readonly 'plotlines.kind': "类型";
109
+ readonly 'plotlines.kindMain': "主线";
110
+ readonly 'plotlines.kindBranch': "支线";
111
+ readonly 'plotlines.kindCharacter': "人物线";
112
+ readonly 'plotlines.kindMystery': "悬念线";
113
+ readonly 'plotlines.goal': "目标/终点";
114
+ readonly 'plotlines.progress': "当前进度";
115
+ readonly 'plotlines.status': "状态";
116
+ readonly 'plotlines.statusActive': "推进中";
117
+ readonly 'plotlines.statusPaused': "暂停";
118
+ readonly 'plotlines.statusResolved': "已完结";
119
+ readonly 'plotlines.statusAbandoned': "已废弃";
120
+ readonly 'plotlines.save': "保存";
121
+ readonly 'plotlines.cancel': "取消";
122
+ readonly 'plotlines.edit': "编辑";
123
+ readonly 'plotlines.remove': "删除";
124
+ readonly 'plotlines.linkChapter': "关联本章";
125
+ readonly 'plotlines.chapters': "已关联";
126
+ readonly 'plotlines.workflowTitle': "剧情线进度";
127
+ readonly 'plotlines.workflowEmpty': "未建剧情线(去「剧情线」页创建)";
128
+ readonly 'sensitive.title': "敏感词检查";
129
+ readonly 'sensitive.scanAll': "🔞 全书敏感词检查";
130
+ readonly 'sensitive.hint': "内置违禁词库硬匹配(政治/擦边/暴力/辱骂/广告/其他),发布前建议检查;只做提示,是否修改由你判断。";
131
+ readonly 'sensitive.clean': "✅ 未命中任何违禁词(扫描 {n} 章)";
132
+ readonly 'sensitive.hits': "命中 {n} 处({chapters} 章受影响)";
133
+ readonly 'sensitive.goFix': "去修订";
134
+ readonly 'sensitive.fixPrefill': "请删除或替换正文中的违禁表达「{word}」(类别:{category}),用含蓄/间接的写法替代。";
135
+ readonly 'facts.title': "编年录 / 时间线({n} 条)";
136
+ readonly 'facts.hint': "每章生成后自动抽取「已确立事实」(人物状态/境界资源/关系变化/伏笔落地),最近 20 条注入后续章节生成,保证长期一致。";
137
+ readonly 'facts.backfill': "回填历史章节事实";
138
+ readonly 'facts.backfilled': "已从历史章节回填 {n} 条事实";
102
139
  readonly 'settings.title': "设置";
103
140
  readonly 'settings.outlinePath': "默认大纲路径";
104
141
  readonly 'settings.outputDir': "输出目录";
105
142
  readonly 'settings.provider': "模型提供商";
106
143
  readonly 'settings.model': "模型";
144
+ readonly 'settings.theme': "主题";
145
+ readonly 'settings.themeLiquid': "iOS 液态玻璃(绿)";
146
+ readonly 'settings.themeClassic': "经典毛玻璃(蓝)";
147
+ readonly 'settings.themeNeumorph': "新拟物(双阴影)";
148
+ readonly 'settings.themeHint': "存于浏览器本地,即时生效";
107
149
  readonly 'settings.chapterChars': "每章目标字数";
108
150
  readonly 'settings.maxTokens': "单章最大输出 tokens";
109
151
  readonly 'settings.reviewPassScore': "审稿通过分数(0-100)";
@@ -1,7 +1,7 @@
1
1
  import type { NovelApi } from '../api.ts';
2
2
  import type { PanelController } from './controller.ts';
3
3
  /** The panel's tab identifiers. */
4
- export type NovelTab = 'workflow' | 'overview' | 'blurb' | 'plan' | 'bible' | 'world' | 'foreshadow' | 'assistant' | 'settings' | 'characters' | 'assetsGenre' | 'assetsProgression' | 'assetsTemplates' | 'assetsRules' | 'assetsStyle';
4
+ export type NovelTab = 'workflow' | 'overview' | 'blurb' | 'plan' | 'bible' | 'world' | 'foreshadow' | 'assistant' | 'settings' | 'characters' | 'facts' | 'plotlines' | 'assetsGenre' | 'assetsProgression' | 'assetsTemplates' | 'assetsRules' | 'assetsStyle';
5
5
  /** Panel shell props. */
6
6
  export interface NovelPanelProps {
7
7
  /** The panel state owner (open/close/toggle). */
@@ -17,6 +17,12 @@ export declare function inferBookName(outline: string): string;
17
17
  export declare function loadProject(outputDir: string): ProjectState | undefined;
18
18
  /** Persist the project state next to the chapters. */
19
19
  export declare function saveProject(outputDir: string, project: ProjectState): void;
20
+ /** 对一段文本做违禁词硬匹配,返回命中(词/类别/次数)。 */
21
+ export declare function checkSensitiveText(text: string): Array<{
22
+ word: string;
23
+ category: string;
24
+ count: number;
25
+ }>;
20
26
  /** List generated chapter files in the output dir (sorted). */
21
27
  export declare function listChapterFiles(outputDir: string): string[];
22
28
  /** Re-sync chapter status against files on disk (a file may exist without state). */
@@ -53,6 +53,10 @@ export declare const NOVEL_API: {
53
53
  readonly world: "/api/dsh-novel-forge/world";
54
54
  /** 封面:GET 读取(dataUrl)/ POST 上传或移除。 */
55
55
  readonly cover: "/api/dsh-novel-forge/blurb/cover";
56
+ /** 剧情线管理:增删改 + 关联章节。 */
57
+ readonly plotlines: "/api/dsh-novel-forge/plotlines";
58
+ /** 敏感词检查:全书已写章节或指定文本。 */
59
+ readonly sensitiveCheck: "/api/dsh-novel-forge/sensitive-check";
56
60
  readonly config: "/api/dsh-novel-forge/config";
57
61
  readonly openFolder: "/api/dsh-novel-forge/open-folder";
58
62
  };
@@ -178,6 +182,8 @@ export interface CharacterCard {
178
182
  goals: string;
179
183
  /** Key relations to other characters. */
180
184
  relations: string;
185
+ /** 知情度:该角色已经知道的事实/秘密(未列出的信息该角色不知道)。 */
186
+ knowledge?: string[];
181
187
  }
182
188
  /** The structured story bible (worldbuilding extracted from the outline). */
183
189
  export interface StoryBible {
@@ -234,6 +240,65 @@ export interface AuditResponse {
234
240
  /** 质检时间。 */
235
241
  auditedAt: string;
236
242
  }
243
+ /** 一条剧情线(主线/支线/人物线/悬念线)。 */
244
+ export interface Plotline {
245
+ /** 稳定 id。 */
246
+ id: string;
247
+ /** 线名。 */
248
+ name: string;
249
+ /** 类型:主线 / 支线 / 人物线 / 悬念线。 */
250
+ kind: 'main' | 'branch' | 'character' | 'mystery';
251
+ /** 目标/终点(这条线最终要完成什么)。 */
252
+ goal: string;
253
+ /** 当前进度说明(最近推进到哪)。 */
254
+ progress: string;
255
+ /** 生命周期状态。 */
256
+ status: 'active' | 'paused' | 'resolved' | 'abandoned';
257
+ /** 关联章节号(推进/落地的章节)。 */
258
+ chapters: number[];
259
+ /** 创建时间。 */
260
+ createdAt: string;
261
+ }
262
+ /** POST /plotlines 请求:剧情线增删改 + 关联章节。 */
263
+ export interface PlotlinesRequest {
264
+ op: 'add' | 'update' | 'remove' | 'link';
265
+ /** add / update 时传入的完整剧情线。 */
266
+ line?: Plotline;
267
+ /** remove / link 时的目标线 id。 */
268
+ id?: string;
269
+ /** link 时关联的章节号。 */
270
+ chapterNo?: number;
271
+ }
272
+ /** POST /plotlines 响应。 */
273
+ export interface PlotlinesResponse {
274
+ plotlines: Plotline[];
275
+ }
276
+ /** 一条敏感词命中。 */
277
+ export interface SensitiveHit {
278
+ /** 命中章节号(文本检测时为 0)。 */
279
+ chapterNo: number;
280
+ /** 命中的违禁词。 */
281
+ word: string;
282
+ /** 类别:政治 / 擦边 / 暴力 / 辱骂 / 广告 / 其他。 */
283
+ category: string;
284
+ /** 出现次数。 */
285
+ count: number;
286
+ }
287
+ /** POST /sensitive-check 请求:检测指定章节/任意文本/全书。 */
288
+ export interface SensitiveCheckRequest {
289
+ /** 检测该章正文。 */
290
+ chapterNo?: number;
291
+ /** 检测任意文本(优先于 chapterNo)。 */
292
+ text?: string;
293
+ /** 扫描全部已写章节。 */
294
+ all?: boolean;
295
+ }
296
+ /** POST /sensitive-check 响应。 */
297
+ export interface SensitiveCheckResponse {
298
+ hits: SensitiveHit[];
299
+ /** 参与扫描的章节数。 */
300
+ scannedChapters: number;
301
+ }
237
302
  /** 角色卡:角色当前状态(从事实库聚合)。 */
238
303
  export interface RoleStatusCard {
239
304
  name: string;
@@ -251,6 +316,8 @@ export interface BiblePatchRequest {
251
316
  worldRules?: string[];
252
317
  redLines?: string[];
253
318
  style?: string[];
319
+ /** 角色卡整体替换(人物志编辑知情度等)。 */
320
+ characters?: CharacterCard[];
254
321
  }
255
322
  /** POST /blurb 请求:AI 生成/补全或手动保存小说简介。 */
256
323
  export interface BlurbRequest {
@@ -351,6 +418,8 @@ export interface ProjectState {
351
418
  coverPath?: string;
352
419
  /** 大世界结构化数据(境界体系/区域/势力)。 */
353
420
  world?: WorldState;
421
+ /** 剧情线(主线/支线/人物线/悬念线)。 */
422
+ plotlines?: Plotline[];
354
423
  /** ISO timestamps. */
355
424
  createdAt: string;
356
425
  updatedAt: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@waterwx/dsh-novel-forge",
3
3
  "description": "AI 编译小说工作台 for the dsh web GUI: import a novel outline (docx/text), plan chapters with LLM, generate chapter by chapter (3000-4000 chars each), and save every chapter as Markdown into your chosen output folder.",
4
- "version": "1.1.2",
4
+ "version": "1.1.3",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
package/src/client/api.ts CHANGED
@@ -162,6 +162,16 @@ export class NovelApi {
162
162
  return postJson<{ bible: import('../protocol.ts').StoryBible }>(NOVEL_API.biblePatch, patch)
163
163
  }
164
164
 
165
+ /** 剧情线管理:增删改 + 关联章节。 */
166
+ async plotlines(req: import('../protocol.ts').PlotlinesRequest): Promise<import('../protocol.ts').PlotlinesResponse> {
167
+ return postJson<import('../protocol.ts').PlotlinesResponse>(NOVEL_API.plotlines, req)
168
+ }
169
+
170
+ /** 敏感词检查:指定章节 / 任意文本 / 全书。 */
171
+ async sensitiveCheck(req: import('../protocol.ts').SensitiveCheckRequest): Promise<import('../protocol.ts').SensitiveCheckResponse> {
172
+ return postJson<import('../protocol.ts').SensitiveCheckResponse>(NOVEL_API.sensitiveCheck, req)
173
+ }
174
+
165
175
  /** 小说简介:AI 生成/补全(partial 留空 = 全量),或手动保存。 */
166
176
  async blurb(action: 'generate' | 'save', text?: string, partial?: string): Promise<{ blurb: string }> {
167
177
  return postJson<{ blurb: string }>(NOVEL_API.blurb, { action, text, partial })
@@ -100,11 +100,53 @@ export const zh = {
100
100
  'foreshadow.plantedAt': '埋设于',
101
101
  'foreshadow.setPlanted': '标记已埋设',
102
102
  'foreshadow.setResolved': '标记已回收',
103
+ 'tab.facts': '编年录',
104
+ 'tab.plotlines': '剧情线',
105
+ 'plotlines.hint': '主线/支线/人物线/悬念线的推进管理:章节生成时会注入未完结的线,要求每章至少推进一条;关联章节记录推进节点。',
106
+ 'plotlines.empty': '还没有剧情线——长篇小说建议先建主线与几条支线。',
107
+ 'plotlines.new': '+ 新建剧情线',
108
+ 'plotlines.name': '线名',
109
+ 'plotlines.kind': '类型',
110
+ 'plotlines.kindMain': '主线',
111
+ 'plotlines.kindBranch': '支线',
112
+ 'plotlines.kindCharacter': '人物线',
113
+ 'plotlines.kindMystery': '悬念线',
114
+ 'plotlines.goal': '目标/终点',
115
+ 'plotlines.progress': '当前进度',
116
+ 'plotlines.status': '状态',
117
+ 'plotlines.statusActive': '推进中',
118
+ 'plotlines.statusPaused': '暂停',
119
+ 'plotlines.statusResolved': '已完结',
120
+ 'plotlines.statusAbandoned': '已废弃',
121
+ 'plotlines.save': '保存',
122
+ 'plotlines.cancel': '取消',
123
+ 'plotlines.edit': '编辑',
124
+ 'plotlines.remove': '删除',
125
+ 'plotlines.linkChapter': '关联本章',
126
+ 'plotlines.chapters': '已关联',
127
+ 'plotlines.workflowTitle': '剧情线进度',
128
+ 'plotlines.workflowEmpty': '未建剧情线(去「剧情线」页创建)',
129
+ 'sensitive.title': '敏感词检查',
130
+ 'sensitive.scanAll': '🔞 全书敏感词检查',
131
+ 'sensitive.hint': '内置违禁词库硬匹配(政治/擦边/暴力/辱骂/广告/其他),发布前建议检查;只做提示,是否修改由你判断。',
132
+ 'sensitive.clean': '✅ 未命中任何违禁词(扫描 {n} 章)',
133
+ 'sensitive.hits': '命中 {n} 处({chapters} 章受影响)',
134
+ 'sensitive.goFix': '去修订',
135
+ 'sensitive.fixPrefill': '请删除或替换正文中的违禁表达「{word}」(类别:{category}),用含蓄/间接的写法替代。',
136
+ 'facts.title': '编年录 / 时间线({n} 条)',
137
+ 'facts.hint': '每章生成后自动抽取「已确立事实」(人物状态/境界资源/关系变化/伏笔落地),最近 20 条注入后续章节生成,保证长期一致。',
138
+ 'facts.backfill': '回填历史章节事实',
139
+ 'facts.backfilled': '已从历史章节回填 {n} 条事实',
103
140
  'settings.title': '设置',
104
141
  'settings.outlinePath': '默认大纲路径',
105
142
  'settings.outputDir': '输出目录',
106
143
  'settings.provider': '模型提供商',
107
144
  'settings.model': '模型',
145
+ 'settings.theme': '主题',
146
+ 'settings.themeLiquid': 'iOS 液态玻璃(绿)',
147
+ 'settings.themeClassic': '经典毛玻璃(蓝)',
148
+ 'settings.themeNeumorph': '新拟物(双阴影)',
149
+ 'settings.themeHint': '存于浏览器本地,即时生效',
108
150
  'settings.chapterChars': '每章目标字数',
109
151
  'settings.maxTokens': '单章最大输出 tokens',
110
152
  'settings.reviewPassScore': '审稿通过分数(0-100)',
@@ -239,6 +281,11 @@ export const en: Record<string, string> = {
239
281
  'settings.outputDir': 'Output directory',
240
282
  'settings.provider': 'Provider',
241
283
  'settings.model': 'Model',
284
+ 'settings.theme': 'Theme',
285
+ 'settings.themeLiquid': 'iOS Liquid Glass (green)',
286
+ 'settings.themeClassic': 'Classic frosted (blue)',
287
+ 'settings.themeNeumorph': 'Neumorphism (dual-shadow)',
288
+ 'settings.themeHint': 'Stored in browser localStorage; applies instantly',
242
289
  'settings.chapterChars': 'Chars per chapter',
243
290
  'settings.maxTokens': 'Max output tokens',
244
291
  'settings.reviewPassScore': 'Review pass score (0-100)',
@@ -252,6 +299,43 @@ export const en: Record<string, string> = {
252
299
  'settings.exportTxt': 'Export TXT',
253
300
  'settings.exportMd': 'Export Markdown',
254
301
  'settings.exported': 'Exported: {file} ({chars} chars, {chapters} chapters)',
302
+ 'tab.facts': 'Chronicle',
303
+ 'tab.plotlines': 'Plotlines',
304
+ 'plotlines.hint': 'Track main/branch/character/mystery arcs: unfinished arcs are injected into chapter writing so each chapter advances at least one; linked chapters record progress nodes.',
305
+ 'plotlines.empty': 'No plotlines yet — consider creating the main arc and a few branches.',
306
+ 'plotlines.new': '+ New plotline',
307
+ 'plotlines.name': 'Name',
308
+ 'plotlines.kind': 'Type',
309
+ 'plotlines.kindMain': 'Main',
310
+ 'plotlines.kindBranch': 'Branch',
311
+ 'plotlines.kindCharacter': 'Character',
312
+ 'plotlines.kindMystery': 'Mystery',
313
+ 'plotlines.goal': 'Goal',
314
+ 'plotlines.progress': 'Current progress',
315
+ 'plotlines.status': 'Status',
316
+ 'plotlines.statusActive': 'Active',
317
+ 'plotlines.statusPaused': 'Paused',
318
+ 'plotlines.statusResolved': 'Resolved',
319
+ 'plotlines.statusAbandoned': 'Abandoned',
320
+ 'plotlines.save': 'Save',
321
+ 'plotlines.cancel': 'Cancel',
322
+ 'plotlines.edit': 'Edit',
323
+ 'plotlines.remove': 'Remove',
324
+ 'plotlines.linkChapter': 'Link chapter',
325
+ 'plotlines.chapters': 'Linked',
326
+ 'plotlines.workflowTitle': 'Plotline progress',
327
+ 'plotlines.workflowEmpty': 'No plotlines yet (create them in the Plotlines tab)',
328
+ 'sensitive.title': 'Sensitive-word check',
329
+ 'sensitive.scanAll': '🔞 Scan all chapters',
330
+ 'sensitive.hint': 'Built-in banned-word library (politics/erotica/violence/abuse/ads/other). Run before publishing; suggestions only.',
331
+ 'sensitive.clean': '✅ No banned words found ({n} chapters scanned)',
332
+ 'sensitive.hits': '{n} hits across {chapters} chapters',
333
+ 'sensitive.goFix': 'Fix',
334
+ 'sensitive.fixPrefill': 'Remove or rephrase the banned expression "{word}" (category: {category}) with an indirect alternative.',
335
+ 'facts.title': 'Chronicle / timeline ({n} facts)',
336
+ 'facts.hint': 'Auto-extracted established facts per chapter (state/resources/relations/foreshadow landings); the latest 20 are injected into later chapters for consistency.',
337
+ 'facts.backfill': 'Backfill facts from historical chapters',
338
+ 'facts.backfilled': 'Backfilled {n} facts from historical chapters',
255
339
  'progress.generating': 'Writing chapter {no} “{title}”…',
256
340
  'progress.done': 'Chapter {no} done ({chars} chars) → {file}',
257
341
  'progress.reviewed': 'Chapter {no} review: {score} — {verdict}',
@@ -205,7 +205,7 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
205
205
  const genreLibrary = data.genreLibrary
206
206
 
207
207
  return (
208
- <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
208
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, flex: 1, minHeight: 0 }}>
209
209
  {error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{tt('common.error')}: {error}</span></div>}
210
210
  {notice !== '' && <div className={css.card}><span style={{ color: 'var(--nf-success)' }}>{notice}</span></div>}
211
211
 
@@ -254,7 +254,7 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
254
254
 
255
255
  {/* 推进模式库 */}
256
256
  {assetTab === 'progression' && (
257
- <div className={css.card}>
257
+ <div className={css.card} style={{ flex: 1, minHeight: 0 }}>
258
258
  <span className={css.cardTitle}>推进模式库</span>
259
259
  <span className={css.meta}>读者为什么继续看下一章?主模式 + 辅助模式注入卷规划与章节生成。</span>
260
260
  {assets.primaryProgression !== undefined && (
@@ -268,7 +268,7 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
268
268
  </div>
269
269
  ))}
270
270
  <span className={css.meta}>从内置推进模式库选择添加(第一个设为主推进):</span>
271
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 260, overflowY: 'auto' }}>
271
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1, minHeight: 0, overflowY: 'auto' }}>
272
272
  {data.progressionLibrary.map(mode => {
273
273
  const alreadyPrimary = assets.primaryProgression?.name === mode.name
274
274
  const alreadyAux = assets.auxiliaryProgressions.some(m => m.name === mode.name)
@@ -285,10 +285,10 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
285
285
 
286
286
  {/* 笔法帖(一键绑定) */}
287
287
  {assetTab === 'templates' && (
288
- <div className={css.card}>
288
+ <div className={css.card} style={{ flex: 1, minHeight: 0 }}>
289
289
  <span className={css.cardTitle}>笔法帖</span>
290
290
  <span className={css.meta}>从内置 8 套叙事风格模板中一键选用(来自 AI-Novel-Writing-Assistant 写法引擎),无需样本文本;绑定后生成与润色都遵循该风格。</span>
291
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8, maxHeight: 420, overflowY: 'auto' }}>
291
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1, minHeight: 0, overflowY: 'auto' }}>
292
292
  {data.styleTemplates.map(template => {
293
293
  const bound = assets.styleAssets.some(s => s.name === template.name)
294
294
  return (
@@ -325,10 +325,10 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
325
325
 
326
326
  {/* 文戒 */}
327
327
  {assetTab === 'rules' && (
328
- <div className={css.card}>
328
+ <div className={css.card} style={{ flex: 1, minHeight: 0 }}>
329
329
  <span className={css.cardTitle}>文戒</span>
330
330
  <span className={css.meta}>写作时必须遵守的表达边界(内置全局 + 项目自定义),生成与审稿都会检查。内置规则可用「覆盖编辑」复制为自定义版本调整。</span>
331
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 280, overflowY: 'auto' }}>
331
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1, minHeight: 0, overflowY: 'auto' }}>
332
332
  {builtinRules.map(rule => {
333
333
  const overridden = customRules.some(r => r.name === rule.name)
334
334
  return (