Compare commits

..

2 Commits

Author SHA1 Message Date
AncTe
5e611d3a00
Merge bf6702be54 into 1a569804d8 2026-07-16 12:34:39 +00:00
bf6702be54 feat: 录像系统 2026-07-16 20:34:28 +08:00
9 changed files with 529 additions and 254 deletions

View File

@ -2,11 +2,13 @@ import { logger } from '@motajs/common';
import {
IReplayArray,
IReplayArrayConfig,
IReplayArraySave,
IReplayReadStream,
IReplayStepHandler,
ReplayCommandWidth,
ReplayParamValue
} from './types';
import { ISaveableContent } from '../save';
interface INormalizedParam {
/**
@ -44,7 +46,9 @@ interface IDecodedCommand {
readonly paramCount: number;
}
export class ReplayArray implements IReplayArray {
export class ReplayArray
implements IReplayArray, ISaveableContent<IReplayArraySave>
{
length: number = 0;
commandWidth: ReplayCommandWidth = ReplayCommandWidth.Uint8;
@ -705,6 +709,9 @@ export class ReplayArray implements IReplayArray {
expired: false,
read: () => {
if (stream.expired) {
logger.warn(155);
}
if (stream.index >= this.length) return null;
const { command, paramCount } = this.decodeCommand(currCommand);
const params = this.decodeParamList(currParam, paramCount);
@ -787,4 +794,30 @@ export class ReplayArray implements IReplayArray {
getParamArray(): ArrayBuffer {
return this.paramBuffer;
}
//#region 存读档
saveState(): IReplayArraySave {
return {
commands: this.commandBuffer,
params: this.paramBuffer,
metadata: {
commandWidth: this.commandWidth
}
};
}
loadState(state: IReplayArraySave): void {
this.commandBuffer = state.commands;
this.paramBuffer = state.params;
this.commandWidth = state.metadata.commandWidth;
this.commandView = new DataView(this.commandBuffer);
this.paramView = new DataView(this.paramBuffer);
this.commandArray = new Uint8Array(this.commandBuffer);
this.paramArray = new Uint8Array(this.paramBuffer);
this.indexArray = new Uint32Array(this.indexBuffer);
this.expireStreams();
}
}

View File

@ -0,0 +1,183 @@
import { logger } from '@motajs/common';
import { IReplaySystem } from './types';
interface IReplaySafetyCollection {
/** 这个安全对象(由 `@shouldReplay()` 触发)的名称,即传入给 `@shouldReplay` 的参数 */
readonly name: string;
/** 这个安全对象所包含的所有字对象 */
readonly messages: IReplaySafetyCollection[];
}
interface IReplaySafetyDetailQueue {
/** 详细信息代码 */
readonly code: number;
/** 此详细信息对应的安全对象 */
readonly collection: IReplaySafetyCollection;
}
type ReplayDecorator = <
Return,
This,
Func extends (this: This, ...args: any[]) => Return
>(
method: Func,
context: ClassMethodDecoratorContext
) => (this: This, ...args: any[]) => Return;
/** 本次收集使用的录像系统 */
let replaySystem: IReplaySystem | null = null;
/** 当前是否正在进行录像收集 */
let collecting: boolean = false;
/** 收集之前录像系统的录像长度 */
let beforeLength: number = 0;
/** 收集时是否有 `@ignoreReplay` 调用 */
let shouldIgnore: boolean = false;
/** 录像安全收集根对象 */
const collection: IReplaySafetyCollection = {
name: 'root',
messages: []
};
/** 当前录像安全收集对象 */
let currentCollection: IReplaySafetyCollection | null = null;
/** 详细信息 code */
let detailCode = 0;
/** 录像收集详细信息队列,保留 50 个以确保可以在控制台重复输出 */
const detailQueue: IReplaySafetyDetailQueue[] = [];
/**
*
* @param system
*/
export function beginReplaySafetyCollection(system: IReplaySystem): void {
if (collecting || replaySystem) {
logger.warn(159);
return;
}
replaySystem = system;
collecting = true;
beforeLength = system.route.length;
collection.messages.length = 0;
currentCollection = collection;
}
/**
*
*/
export function endReplaySafetyCollection(): void {
if (!collecting || !replaySystem) {
logger.warn(160);
return;
}
if (shouldIgnore) return;
if (replaySystem.route.length > beforeLength) return;
if (collection.messages.length === 0) return;
// 需要把收集内容输出,这里只输出一层,完整输出需要在控制台手动调用
const code = detailCode++;
const col: IReplaySafetyCollection = {
name: `detail#${code}`,
messages: collection.messages.slice()
};
const command = `Mota.require('@user/data-common').logReplaySafetyDetail(${code});`;
const simplified = [...collection.messages]
.sort((a, b) => b.messages.length - a.messages.length)
.map(v => v.name)
.join('\n');
logger.warn(161, command, simplified);
while (detailQueue.length > 50) {
detailQueue.shift();
}
detailQueue.push({
code,
collection: col
});
}
/**
*
* @param collection
*/
function logCollection(collection: IReplaySafetyCollection): void {
console.group(collection.name);
const sorted = collection.messages.toSorted(
(a, b) => b.messages.length - a.messages.length
);
for (const msg of sorted) {
if (msg.messages.length === 0) {
console.log(msg.name);
} else {
logCollection(msg);
}
}
console.groupEnd();
}
/**
*
* @param code
*/
export function logReplaySafetyDetail(code: number): void {
const root = detailQueue.find(v => v.code === code);
if (!root) {
logger.warn(162, code.toString());
return;
}
logCollection(root.collection);
}
/**
*
* @param message 使
*/
export function shouldReplay(message: string): ReplayDecorator {
return function <
Return,
This,
Args extends any[],
Func extends (this: This, ...args: Args) => Return
>(
this: This,
method: Func,
context: ClassMethodDecoratorContext
): (this: This, ...args: Args) => Return {
return function (this: This, ...args: Args): Return {
const before = currentCollection;
if (!before) return method.apply(this, args);
const exist = before.messages.find(v => v.name === message);
const newCollection: IReplaySafetyCollection = exist ?? {
name: `${String(context.name)}: ${message}`,
messages: []
};
if (!exist) {
before.messages.push(newCollection);
}
currentCollection = newCollection;
const result = method.apply(this, args);
currentCollection = before;
return result;
};
};
}
/**
*
* 使
*/
export function ignoreReplay(): ReplayDecorator {
return function <
Return,
This,
Args extends any[],
Func extends (this: This, ...args: Args) => Return
>(this: This, method: Func): (this: This, ...args: Args) => Return {
return function (this: This, ...args: Args): Return {
shouldIgnore = true;
return method.apply(this, args);
};
};
}

View File

@ -1,2 +1,3 @@
export * from './array';
export * from './sandbox';
export * from './types';

View File

@ -0,0 +1,146 @@
import {
Hookable,
HookController,
IHookController,
logger
} from '@motajs/common';
import {
IReplayArray,
IReplayReadStream,
IReplaySandbox,
IReplaySandboxHook,
IReplaySystem
} from './types';
export class ReplaySandbox
extends Hookable<IReplaySandboxHook>
implements IReplaySandbox
{
pausing: boolean = true;
speed: number = 1;
ended: boolean = false;
playing: boolean = false;
/** 下一步是否需要暂停播放 */
private needPause: boolean = false;
/** 当前录像是否播放完毕 */
private ending: boolean = false;
/** 暂停 `Promise` 的 `resolve` 函数 */
private pauseResolve: () => void = () => {};
/** 录像的流式读取器 */
private reader: Readonly<IReplayReadStream>;
constructor(
readonly route: IReplayArray,
readonly system: IReplaySystem,
startIndex: number
) {
super();
this.reader = route.createReadStream(startIndex);
}
protected createController(
hook: Partial<IReplaySandboxHook>
): IHookController<IReplaySandboxHook> {
return new HookController(this, hook);
}
setSpeed(speed: number): void {
this.speed = speed;
this.forEachHook(hook => hook.onSpeedSet?.(speed));
}
getReplayed(): number {
return this.reader.index;
}
/**
*
*/
private async startReplayLoop() {
if (this.ended) return;
if (this.reader.expired) {
logger.warn(156);
return;
}
while (true) {
if (this.needPause && !this.pausing) {
this.pausing = true;
this.needPause = false;
this.pauseResolve();
this.forEachHook(hook => hook.onPauseReplay?.());
break;
}
if (this.reader.expired) {
logger.warn(156);
break;
}
const success = await this.step();
if (!success) break;
}
if (this.ending) {
this.ended = true;
}
}
play(): void {
if (this.playing || this.ended) return;
this.pausing = false;
this.playing = true;
this.forEachHook(hook => hook.onStartReplay?.());
this.startReplayLoop();
}
pause(): Promise<void> {
if (this.pausing || this.ended || !this.playing) {
return Promise.resolve();
}
const { promise, resolve } = Promise.withResolvers<void>();
this.pauseResolve = resolve;
this.needPause = true;
return promise;
}
resume(): void {
if (!this.pausing || !this.playing || this.ended) return;
this.pausing = false;
this.forEachHook(hook => hook.onResumeReplay?.());
this.startReplayLoop();
}
async stop(): Promise<void> {
if (this.pausing || this.ended || !this.playing) return;
await this.pause();
this.forEachHook(hook => hook.onStopReplay?.());
}
async step(): Promise<boolean> {
if (!this.pausing || !this.playing || this.ended) return false;
if (this.reader.expired) {
logger.warn(156);
return false;
}
const next = this.reader.read();
if (!next) {
this.ending = true;
return false;
}
const command = this.system.getCommand(next.command);
if (!command) {
logger.warn(157, next.command.toString());
return false;
}
const success = await command.execute(next);
if (!success) {
logger.warn(
158,
next.command.toString(),
JSON.stringify(next.params)
);
return false;
}
await Promise.all(this.forEachHook(hook => hook.onStep?.(next)));
return true;
}
}

View File

@ -0,0 +1,87 @@
import {
Hookable,
HookController,
IHookController,
logger
} from '@motajs/common';
import {
IReplayArray,
IReplayCommand,
IReplaySandbox,
IReplaySandboxConfig,
IReplaySystem,
IReplaySystemHook,
ReplayCommandWidth,
ReplayParamValue
} from './types';
import { ReplayArray } from './array';
import { ReplaySandbox } from './sandbox';
export class ReplaySystem
extends Hookable<IReplaySystemHook>
implements IReplaySystem
{
replaying: boolean = false;
sandbox: IReplaySandbox | null = null;
route: IReplayArray;
/** 所有注册的指令 */
private readonly commands: Map<number, IReplayCommand> = new Map();
constructor() {
super();
this.route = new ReplayArray({
initCommandLength: 1000,
initParamLength: 10000,
commandExpandMultiplier: 1.2,
paramExpandMultiplier: 1.2,
commandWidth: ReplayCommandWidth.Uint8,
commandMaxLength: 1_000_000,
paramMaxLength: 10_000_000
});
}
protected createController(
hook: Partial<IReplaySystemHook>
): IHookController<IReplaySystemHook> {
return new HookController(this, hook);
}
registerCommand(code: number, command: IReplayCommand): void {
if (this.commands.has(code)) {
logger.warn(163);
return;
}
this.commands.set(code, command);
}
getCommand(code: number): IReplayCommand | null {
return this.commands.get(code) ?? null;
}
record(code: number, ...params: ReplayParamValue[]): void {
this.route.add(code, params);
this.forEachHook(hook =>
hook.onRecordCommand?.(code, this.route.length, params)
);
}
createReplaySandbox(
config: Readonly<IReplaySandboxConfig>
): IReplaySandbox {
config.reseter.reset(config.save);
const sandbox = new ReplaySandbox(
config.route,
this,
config.startIndex ?? 0
);
this.sandbox = sandbox;
this.forEachHook(hook => hook.onCreateSandbox?.(sandbox));
return sandbox;
}
releaseSandbox(): void {
this.sandbox?.stop();
this.sandbox = null;
}
}

View File

@ -1,5 +1,4 @@
import { IHookable, IHookBase } from '@motajs/common';
import { ISaveableContent } from '../save';
export type ReplayParamValue = number | string | boolean | bigint;
@ -16,8 +15,9 @@ export interface IReplayCommand {
/**
*
* @param step
* @returns `Promise`
*/
execute(step: IReplayStepHandler): Promise<void>;
execute(step: IReplayStepHandler): Promise<boolean>;
}
export interface IReplaySandboxHook extends IHookBase {
@ -57,8 +57,12 @@ export interface IReplaySandboxHook extends IHookBase {
export interface IReplaySandbox extends IHookable<IReplaySandboxHook> {
/** 是否处于暂停状态 */
readonly pausing: boolean;
/** 当前录像是否已经播放完毕 */
readonly ended: boolean;
/** 播放倍率1 为正常速度 */
readonly speed: number;
/** 当前是否正在播放,当调用 `play` 后,调用 `stop` 前,此值会是 `true` */
readonly playing: boolean;
/**
*
@ -66,6 +70,11 @@ export interface IReplaySandbox extends IHookable<IReplaySandboxHook> {
*/
setSpeed(speed: number): void;
/**
*
*/
getReplayed(): number;
/**
*
*/
@ -88,8 +97,9 @@ export interface IReplaySandbox extends IHookable<IReplaySandboxHook> {
/**
* Promise
* @returns
*/
step(): Promise<void>;
step(): Promise<boolean>;
}
export interface IStateReseter {
@ -108,7 +118,7 @@ export const enum ReplayCommandWidth {
}
export interface IReplayReadStream {
/** 当前读取位置的步索引 */
/** 已经读取过的录像步数量,即上一次调用 `read` 时读取的录像步索引 */
index: number;
/** 录像总步数 */
length: number;
@ -144,6 +154,38 @@ export interface IReplayArrayConfig {
paramMaxLength: number;
}
export interface IReplayMetadataSave {
/** 当前录像使用的指令码宽度 */
readonly commandWidth: ReplayCommandWidth;
}
export interface IReplayArraySave {
/**
* Uint8
* Uint16 uint16
*/
readonly commands: ArrayBuffer;
/**
*
*
*
* - 0: boolean --- 2 Byte
* - 1: int8 --- 2 Byte
* - 2: int16 --- 3 Byte
* - 3: int32 --- 5 Byte
* - 4: int64 --- 9 Byte
* - 5: float --- 9 Byte
* - 6: bigint --- n + 1 Byte, n bigint
* - 7: string --- n + 1 Byte, n
* - 8 ~ 255: n - 7 --- n + 1 Byte, n
*/
readonly params: ArrayBuffer;
/** 录像元数据 */
readonly metadata: IReplayMetadataSave;
}
export interface IReplayArray {
/** 录像中的总步数 */
readonly length: number;
@ -260,40 +302,18 @@ export interface IReplaySystemHook extends IHookBase {
): void;
}
export interface IReplayMetadataSave {
/** 当前录像使用的指令码宽度 */
readonly commandWidth: ReplayCommandWidth;
export interface IReplaySandboxConfig {
/** 录像播放沙箱使用的录像数组 */
route: IReplayArray;
/** 录像播放前进行的状态重置使用的状态重置对象 */
reseter: IStateReseter;
/** 录像播放的起始索引,默认为 0 */
startIndex?: number;
/** 初始化使用的存档对象 */
save?: Map<string, unknown>;
}
export interface IReplaySystemSave {
/**
* Uint8
* Uint16 uint16
*/
readonly commands: ArrayBuffer;
/**
*
*
*
* - 0: boolean --- 2 Byte
* - 1: int8 --- 2 Byte
* - 2: int16 --- 3 Byte
* - 3: int32 --- 5 Byte
* - 4: int64 --- 9 Byte
* - 5: float --- 9 Byte
* - 6: bigint --- n + 1 Byte, n bigint
* - 7: string --- n + 1 Byte, n
* - 8 ~ 255: n - 7 --- n + 1 Byte, n
*/
readonly params: ArrayBuffer;
/** 录像元数据 */
readonly metadata: IReplayMetadataSave;
}
export interface IReplaySystem
extends ISaveableContent<IReplaySystemSave>, IHookable<IReplaySystemHook> {
export interface IReplaySystem extends IHookable<IReplaySystemHook> {
/** 当前是否处在录像播放状态 */
readonly replaying: boolean;
/** 当前正在播放的录像沙箱实例 */
@ -303,27 +323,29 @@ export interface IReplaySystem
/**
*
* @param id
* @param code
* @param command
*/
registerCommand(id: number, command: IReplayCommand): void;
registerCommand(code: number, command: IReplayCommand): void;
/**
*
* @param code
*/
getCommand(code: number): IReplayCommand | null;
/**
*
* @param id
* @param code
* @param params
*/
record(id: number, ...params: ReplayParamValue[]): void;
record(code: number, ...params: ReplayParamValue[]): void;
/**
*
* @param reseter
* @param save
* @param config
*/
createReplaySandbox(
reseter: IStateReseter,
save?: Map<string, unknown>
): IReplaySandbox;
createReplaySandbox(config: Readonly<IReplaySandboxConfig>): IReplaySandbox;
/**
*

View File

@ -214,6 +214,15 @@
"151": "0 or 1 is expected for boolean replay array, but got $1.",
"152": "Bigint replay param reached its limit, min for -(2n ** 2047n), and max for 2n ** 2047n - 1n.",
"153": "Up to 255 parameters for a single replay command, but got $1. Overflowed parameters will be ignored.",
"154": "Cannot transfer replay command $1 from uint16 to uint8, since it's greater than 255."
"154": "Cannot transfer replay command $1 from uint16 to uint8, since it's greater than 255.",
"155": "Replay read stream expired, ignore this warning if you do need read expired replay data, or check your replay reading logic.",
"156": "Replay needs a not expired read stream, please recreate a new replay sandbox with a latest read stream.",
"157": "Replay error: Unknown replay command $1.",
"158": "Replay error: Replay step execute failed. Command code: $1, params: $2.",
"159": "Replay safety collection cannot begin while another collection is running.",
"160": "Replay safety collection ending function is called outside replay safety collection unexpectedly.",
"161": "Replay safety detected. Followings are simplified detect info. To get detailed info, run: '$1'.\n$2",
"162": "Cannot find replay safety detail info of code $1. The detail info may expired, or it just has not been existed.",
"163": "Replay command code $1 has already been registered. This will not replace the old registry."
}
}

193
prompt.md
View File

@ -1,193 +0,0 @@
# 核心定位
你的身份不是架构设计者,而是辅助我开发项目与设计系统的起草者。实现功能永远排在最后一名——保证代码结构清晰、便于阅读、尽量减少代码跳转才是最核心的任务。哪怕实现不了功能,也要保证代码结构清晰。你的任务是尽量写出大致正确的代码以减少我的思考负担,而非追求完美。尝试一次性写出让我满意的完美代码往往会适得其反。
该项目的架构已经过大量设计与验证,当前架构是深思熟虑的结果。除非我明确要求,否则不要重新设计架构,也不要主动优化已有设计。你的职责是在保持整体风格与架构稳定的前提下,完成需求并自然扩展已有系统。
# 规则优先级
本文为 AI Agent 专用提示词,我有时可以不遵守,你**必须**遵守。
当多种规则来源产生冲突时,按以下优先级裁定:
1. **已有代码 > 本文**:当前文件中的已有实现是最高优先级规范。当 prompt、已有代码、自身知识冲突时优先遵循已有代码。
2. **不确定时保守**:如果两种方案都能实现需求,默认选择修改最少、风险最低、最接近已有实现的方案。
3. **本文 > dev.md**`dev.md` 中的规则在本文中强度升级:
| dev.md 原文级别 | 本文对应级别 |
| ------------------------- | ------------ |
| 「一般不建议 / 尽量避免」 | **绝对禁止** |
| 「建议 / 最好」 | **必须遵守** |
典型升级项——**绝对禁止**`as` 关键字、`@ts-expect-error`、`getter` 和 `setter`、三元运算符不允许换行。
# 代码规则
## 修改原则
1. **最小修改**:默认不重新设计已有代码,不重构已有实现,不为「更优雅」而改变已有模式。新代码应是原有代码的自然延续。若认为已有代码存在错误,在对话中提出,不要直接修改。
2. **文件边界**:修改文件前务必重读以确保内容最新。修改后必须在对话中说明所有改动的文件及具体内容。不得修改文档「涉及文件」节未提及的文件。所有公共方法必须在文档中提及,不允许在代码中新增未在文档中写明的公共方法。
3. **歧义提问**:遇到任何模糊、不清晰、歧义的地方,立即向我提问,不要自行假设。若完成需求所需的接口尚不存在,立即停止实现并提出疑问,不要擅自新增接口。
## 代码组织
4. **功能聚合**:以「连续阅读一个功能所需的认知切换最少」为目标组织代码。相似功能放在一起,不以修饰符类型分隔。长文件或功能较多的类使用 `#region` 分区,按方法功能划分。
5. **维护成本优先**:允许局部重复、允许多几个变量与 if 分支。绝不鼓励:为减少行数增加抽象、为减少重复增加函数跳转、为追求「现代 TS」使用复杂语法。
6. **文件分配**:一个文件只能包含一个类。若存在多个同类型简单实现类(均 < 50 行且 implements 同一接口必须先向我确认同意后方可放在同一文件
7. **条件语句**:对于同一个等级的条件,如果满足与不满足都需要执行某些代码,那么必须写完成的 `if - else`,不得在 `if` 里面通过 `return` 提前返回来达成 `else` 效果。
## 注释
8. **私有成员必须注释**:所有私有方法与私有成员必须添加 jsDoc 注释,私有方法的参数必须添加注释说明。构造函数的属性声明参数除外。
9. **公共注释唯一**:受保护与公共成员、方法必须在**源头处**添加注释(多数情况为 `interface`)。继承而来的成员,除非功能或描述有变化,否则不得重复添加注释。
10. **方法注释换行**:方法 jsDoc 必须使用换行风格;成员 jsDoc 简短时可用不换行风格。
## 类型与错误处理
11. **允许类型错误**:编写代码时允许出现 TypeScript 类型错误。不要为了修复类型错误而违反上述规则或增加代码复杂度。尤其不得出现 `as` 关键字。
12. **错误必须抛出**:任何理应报错的场景必须使用 `logger` 接口抛出错误或警告,不得使用 `return` 等方法静默处理。`logger` 必须分配合理的 `code`,不得复用无关 code 或使用 `0`。所有场景下,`logger` 都不会影响系统和游戏的正常运行,不应抛出任何异常中断来终止运行。
13. **非空判断**:对象直接用 `if (!object)`;字面量使用 `lodash``isNil``if (isNil(value))`),不得使用 `if (value === undefined)` 等方式。
14. **公共方法**:不得出现仅在类中定义的公共方法,所有公共方法必须先在接口中定义,然后在类中使用 `implements` 实现。
15. **成员只读**:所有需要被实现为类的接口,其成员必须只读,如果有赋值需求,那么应使用相应的方法来完成赋值操作。
## 架构约束
16. **渲染端被动**:任何情况下渲染端不会主动向数据端推送更新。渲染端仅通过钩子与数据端通信,被动获取信息,仅在某些情况下通过钩子影响数据端行为。
# 开发流程
当我提出需求时,若未明确说明直接实现或另有指示,遵循以下流程:
1. 阅读当前代码,分析需求,将需求整理为 markdown 文档放在 `docs/dev` 目录下(含子目录,视情况自行判断)。文档需标注需求细节与代码实现的大体思路。本阶段遇到任何问题应向我提问确认,不得自行假设。
2. 我会针对文档内容提问,你根据设计思路回答。我会指出设计问题,你根据要求更新文档。
3. 我可能对文档做细微调整,实现前请重新仔细阅读最终版本。实现过程中如有问题应向我提问,而非自行决定。
4. 我会粗略阅读你写出的代码并指出明显问题,你需要修改。
5. 我会认真阅读并调整你的代码,形成最终方案。
6. 你需要对比我的最终代码与你自己的代码,总结本次实现中的核心问题,以便后续改进。
# 文档要求
文档不是设计说明书,而是**需求理解报告**。文档的第一目标不是提出最佳方案,而是暴露 Agent 对需求的理解模型,供我人工确认。
在完成接口设计前,必须先明确:
- 需求中明确要求了什么
- 需求中隐含要求了什么
- 当前设计依赖哪些假设
- 如果假设错误会导致什么设计错误
**禁止直接从需求跳跃到接口设计。** 任何接口设计必须能够追溯到前面的需求事实和逻辑推导。
换言之,文档是一份基于若干假设推导出的一种合理设计方案,重点在于从假设推导出设计的过程,而非设计本身。也就是说,文档本身是一道数学推理题,从公理(设计前提)和定义(核心概念定义)出发,通过逻辑推理(设计思路)得出结论(接口分析),每个环节都依赖于前面的环节,最终得出的接口设计需要从逻辑上使人信服。所有的章节最核心的关键词就是**为什么**,每个章节必须写明**为什么会这样**,如果阅读文档后,我仍然不知道"为什么会设计出这个接口",那么这份文档就是失败的。
同时,任何时候都不能写一个接口能干什么事,这个信息对我来说没有任何价值,有价值的是从需求推导出接口设计的逻辑推理过程,也就是**文档写的是为什么****写是什么没有任何意义**。
**绝对注意**:我发给你的文字并不一定全是需求,可能会有很多内容是我经过思考之后得出的结论,这些内容不应当处理为需求,你应该通过需求建立一个合理的逻辑链来得出我给出的结论。典型的结论性语句就是,需要设计什么什么接口、因为什么什么所以怎么怎么样等等。真正的需求应该可以用几个非常简洁的点总结出来。
# 文档结构
按以下格式编写,其余需求自行组织。
- 我会使用 `>` 引用块在文档中批注,因此**不要在文档中使用引用块**。
- 文档控制在 100-250 行,简洁但包含所有必要信息;不擅自修改示例文档格式。
- 一般不需要流程图;若必须使用 `mermaid`
- 示例文档参考 `docs/dev/template.md`,务必认真阅读后再编写文档。一切行文思路严格按照示例文档走,不要按照自己的想法修改。已编写完成的开发文档可能不符合示例文档要求或内容已过时,不要参考。
- 不得出现大片的 `inline-code`,不是不能写,而是不能大片地出现。像接口名等情况正常使用即可。这一条的目的不是为了不让你用 `inline-code`,而是为了让你不去在文档中简单地罗列一堆接口,接口名、方法名等正常情况完全鼓励使用。
- 关于行数和百分比的约束只是帮助你理解文档中哪些内容是重点,哪些不是,并不需要严格遵循,只要重点正确即可。
```md
# 需求综述
描述清楚需求的内容、动机与目的。
# 需求理解
分析需求,指出你对本次需求的理解。每条使用总分结构,先用一个短语总结,然后解释,总结短语不要加粗。
## 明确需求
从任务描述中可以明确得知的直接需求。
## 隐含需求
任务描述中不包含但对任务本身有重大影响的需求。
## 未确定需求
任务描述中不包含且不属于隐含需求的内容,往往是一些语义模糊的内容。没有则不写此节。
# 设计前提
指出本次设计的前提(如同数学公理),本设计的所有内容都基于此前提进行。每条使用总分结构,先用一个短语总结,然后解释,不要加粗。这一章节应该详细解释每个概念是什么,而不是每个概念会导致什么结果,或者它本身会包含什么行为。
# 核心概念定义
详细描述涉及本次任务的核心概念。我会提供若干个需要解释的概念,如有需要可自行添加。这部分必须使用严谨的逻辑阐述,不能是简单一两句话。使用总分的结构描述,先用一句话给出定义“这个概念是什么”,然后再详细解释。
# 接口设计分析
我可能不会给出完整接口设计,你需要自行分析需求补充设计,并在对话中明确指出哪些接口是你补充的。这一章最核心的就是为什么,必须让我知道你为什么要这么设计接口,接口频率为什么是这么多,预期体量为什么是这么大。
## IExample
### 设计思路
分析需求后编写接口的设计思路。重点是分析需求并说明接口是如何设计出来的,而非简单阐述接口内容。这部分是从设计前提和概念定制推导出接口分析的过程,必须详细分析。写的时候,必须有因有果,由于什么需求,所以要设计哪些接口,不得直接说某个接口可以干什么,不得直接说某个接口可以用于哪些场景。只说某个接口的作用没有任何意义,只有从定义和设计前提推导出设计的这个推导过程有意义,所以不要分析接口可以干什么。对于每个接口,必须写明为什么需要这个接口,“由于什么什么需求,因此需要设计什么什么接口”。当我看完后,必须得让我知道为什么要这么设计接口,如果我看完之后还是不知道为什么,那么这个章节写的就是失败的。
### 接口分析
按接口逐一分析成员与方法的预期使用频率。频率分为高 / 中 / 低,指**用户编写此调用的频率**,而非运行时频率或引擎调用频率。使用频率越高,名称长度宜越短。对于成员,必须写明其类型,对于方法,必须写明其参数及类型。接口分析不要包含继承而来的接口,只分析接口本身包含的内容。必须写明为什么预期频率是这么多,如果我看完之后依然没办法知道为什么,说明这个章节写的是失败的。
- 成员 `property: type`:预期频率**高频**。进行分析。
- 方法 `method(param1: type1, ...)`:预期频率**中频**。进行分析。
### 预期体量
写出预期的代码体量并分析原因。不得只写一句预期多少行,必须详细分析,分条阐述。这里不是要实现思路,而是要写为什么某个功能需要这么多行。预期是对代码量的预估,目的是让我了解到你对某个功能复杂度的理解是否正确,不需要非常精确。预期体量是对实现复杂度的预期,而不是对接口定义的预期,因此不要写接口定义预期多少行。当我读完这个章节后,我必须得理解你为什么会认为某个功能需要这么多行,如果不行,说明这个章节写的是失败的。
- 功能一预期 100 行:进行分析。
- 功能二预期 50 行:进行分析。
- 方法 `method` 预期 20 行:进行分析。
---
以上内容应占据文档的 60% 以上。以上内容中不要写任何关于私有方法或私有成员的内容,以上是设计层面的,是暴露给使用者的接口,私有方法和成员不属于设计层面,所以不应该出现。
---
# 实现思路
对于复杂逻辑,分条描述实现思路。简单实现不要写入本节。
## 复杂需求
简述某个复杂需求的实现思路,分条写,写成有序列表。
# 涉及文件
## `@user/package/[folder/]file.ts`
除非必要或我明确提出,一般不建议擅自新增公共方法或成员,必要时可向我提问。不需要写得过细,涵盖重要信息即可。不要出现大段的 `inline-code`(不是不能写,是不要成片地写,该用的时候就应该用,比如接口分析时分条,某些接口名称等等)。描述不要具体到变量或成员名称等,用简短的一两句话来描述即可(针对冒号后面的内容)。写的时候不要把多个接口或成员写到一个条目里面,分开写。
- [ ] 新增 `IInterface` 接口:描述新增动机与目的、用途
- [ ] 新增 `Type` 类型别名:描述新增动机与目的、用途
- [ ] 编写 `Class.method` 方法:描述实现的大体内容
- [ ] 修改 `Class.method` 方法中的部分内容:描述修改内容与目的
### `@motajs/package/[folder/]file.ts`
...
# 待确认问题
如果描述中有歧义或模糊之处,在此列出,此处只允许列出设计相关的问题,不允许列出实现相关的问题。提问必须是以下类型之一:
- **未定义概念**:我没有明确说明某一个名词具体指什么
- **规则冲突**:我的描述中前后产生矛盾
- **语义模糊**:某一个概念或规则可以有多种不同解释方法
- **设计偏好**:对于某个需求可以推导出多种不同设计方案,需要我决定
不允许出现以下低价值问题:
- **风格偏好**:如命名是否合理、`logger` 的文字描述是否合理等
```

View File

@ -1,13 +0,0 @@
# 核心职责
你的职责不是辅助我进行架构设计,也不是进行系统代码编写,而是对我写出的代码进行 review以及测试用例的编写工作。
# Code Review
在 code review 期间,你需要阅读我写出的代码,以及我给出的需求说明,提出我的代码中可能不合理的部分,我会针对这些问题给出回答,或修改相应的代码,执行几轮循环。期间不得修改我写出的代码。
在 review 期间,务必留意 @dev.md 中的代码规范要求,如果出现了某些不合规范的地方,需要在对话中提出,尤其注意 `as` 关键字、方法排序等要求,重点关注写有“不建议”、“建议”、“最好”、“尽量”等字样的规则。
# 测试用例
由于系统构建的不完全,目前还没有推进到需要进行单元测试的环节。