@guanlin4924/devflow-pi 0.1.18 → 0.1.19
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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +197 -18
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ devflow-pi
|
|
|
34
34
|
/usage-stats
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
`/devflow project-init`
|
|
37
|
+
`/devflow project-init` 会先读取当前项目已有的 `.planning/PROJECT.md` 与 `.planning/navigation/INDEX.md`:仅回填仍存在于项目根目录内的已登记前后端路径,删除或越界的历史路径会被忽略。随后可选择复用已有边界、复用边界并重建索引,或手动覆盖。初始化模式使用选择器提供“只读规划(默认)”和“写入初始化”;选择后者并确认,只会创建 Devflow 的 `.planning/` 与 `.context/` 初始化产物,不会修改业务源码、依赖或项目配置。
|
|
38
38
|
|
|
39
39
|
用量统计默认只读取当前项目的隔离会话:`/usage-stats`。需要明确查看历史 OMP 会话时,使用 `/usage-stats omp`,或加筛选条件:`/usage-stats omp --from 2026-08-01 --to 2026-08-10`。历史统计保留真实会话路径,不再把不同项目的长会话文件合并成 `main`;统计只使用 assistant message 的原始 usage,不虚构工具调用成本分摊。
|
|
40
40
|
|
package/package.json
CHANGED
|
@@ -98,11 +98,116 @@ export function planningState(intent: "project-init" | "requirement-draft"): Ses
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
export type ExistingProjectInitialization = {
|
|
102
|
+
projectName?: string;
|
|
103
|
+
frontendPaths: string[];
|
|
104
|
+
backendPaths: string[];
|
|
105
|
+
codeIndexStatus?: string;
|
|
106
|
+
sourcePath: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
type DocumentedProjectLocations = {
|
|
110
|
+
frontendPaths: string[];
|
|
111
|
+
backendPaths: string[];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function markdownTableCells(line: string): string[] | null {
|
|
115
|
+
if (!line.trim().startsWith("|")) return null;
|
|
116
|
+
const cells = line.trim().split("|").slice(1, -1).map(cell => cell.trim());
|
|
117
|
+
return cells.length > 0 ? cells : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function documentedProjectPath(raw: string, cwd: string): string | null {
|
|
121
|
+
const cleaned = raw
|
|
122
|
+
.replaceAll("`", "")
|
|
123
|
+
.replaceAll("~", "")
|
|
124
|
+
.replaceAll("*", "")
|
|
125
|
+
.replace(/\s*[((].*$/, "")
|
|
126
|
+
.trim();
|
|
127
|
+
if (!cleaned) return null;
|
|
128
|
+
const absolute = path.resolve(cwd, cleaned);
|
|
129
|
+
const relative = path.relative(cwd, absolute).replaceAll("\\", "/");
|
|
130
|
+
if (!relative || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) return null;
|
|
131
|
+
if (!existsSync(absolute)) return null;
|
|
132
|
+
return relative;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function documentedLocations(markdown: string, cwd: string): DocumentedProjectLocations {
|
|
136
|
+
const frontendPaths: string[] = [];
|
|
137
|
+
const backendPaths: string[] = [];
|
|
138
|
+
const lines = markdown.split(/\r?\n/);
|
|
139
|
+
for (let index = 0; index < lines.length - 2; index += 1) {
|
|
140
|
+
const header = markdownTableCells(lines[index]);
|
|
141
|
+
const divider = markdownTableCells(lines[index + 1]);
|
|
142
|
+
if (!header || !divider || !divider.every(cell => /^:?-{3,}:?$/.test(cell))) continue;
|
|
143
|
+
const pathColumn = header.findIndex(cell => /^(?:path|路径)$/i.test(cell));
|
|
144
|
+
const typeColumn = header.findIndex(cell => /^(?:type|类型)$/i.test(cell));
|
|
145
|
+
if (pathColumn < 0 || typeColumn < 0) continue;
|
|
146
|
+
for (let rowIndex = index + 2; rowIndex < lines.length; rowIndex += 1) {
|
|
147
|
+
const row = markdownTableCells(lines[rowIndex]);
|
|
148
|
+
if (!row || row.length !== header.length) break;
|
|
149
|
+
const projectPath = documentedProjectPath(row[pathColumn], cwd);
|
|
150
|
+
if (!projectPath) continue;
|
|
151
|
+
const type = row[typeColumn];
|
|
152
|
+
if (/(?:前端|frontend|\bh5\b|小程序|uniapp)/i.test(type)) frontendPaths.push(projectPath);
|
|
153
|
+
if (/(?:后端|backend|服务|\bapi\b)/i.test(type)) backendPaths.push(projectPath);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
frontendPaths: [...new Set(frontendPaths)],
|
|
158
|
+
backendPaths: [...new Set(backendPaths)],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function documentedProjectName(markdown: string): string | undefined {
|
|
163
|
+
const match = markdown.match(/^\s*-\s*\*\*Name\*\*:\s*(.+?)\s*$/mi);
|
|
164
|
+
return match?.[1]?.trim() || undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function documentedCodeIndexStatus(markdown: string): string | undefined {
|
|
168
|
+
const comment = markdown.match(/code_index_status:\s*([^;\-\n]+)/i);
|
|
169
|
+
if (comment?.[1]?.trim()) return comment[1].trim();
|
|
170
|
+
const field = markdown.match(/^\s*-\s*\*\*Code Index Status\*\*:\s*([^\n]+)/mi);
|
|
171
|
+
return field?.[1]?.trim() || undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Reuses only existing, project-local initialization records. Markdown is
|
|
176
|
+
* evidence, not authority by itself: every documented path must still exist
|
|
177
|
+
* beneath the current project root before it can be offered back to the user.
|
|
178
|
+
*/
|
|
179
|
+
export function discoverExistingProjectInitialization(cwd: string): ExistingProjectInitialization | null {
|
|
180
|
+
const sources = [".planning/PROJECT.md", ".planning/navigation/INDEX.md"];
|
|
181
|
+
const records: Array<ExistingProjectInitialization> = [];
|
|
182
|
+
for (const sourcePath of sources) {
|
|
183
|
+
const absolute = path.join(cwd, sourcePath);
|
|
184
|
+
if (!existsSync(absolute)) continue;
|
|
185
|
+
const markdown = readFileSync(absolute, "utf8");
|
|
186
|
+
const locations = documentedLocations(markdown, cwd);
|
|
187
|
+
if (locations.frontendPaths.length === 0 && locations.backendPaths.length === 0) continue;
|
|
188
|
+
records.push({
|
|
189
|
+
projectName: documentedProjectName(markdown),
|
|
190
|
+
frontendPaths: locations.frontendPaths,
|
|
191
|
+
backendPaths: locations.backendPaths,
|
|
192
|
+
codeIndexStatus: documentedCodeIndexStatus(markdown),
|
|
193
|
+
sourcePath,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const preferred = records[0];
|
|
197
|
+
if (!preferred) return null;
|
|
198
|
+
const fallback = records.find(record => record !== preferred);
|
|
199
|
+
const useFallbackFrontend = preferred.frontendPaths.length === 0 && Boolean(fallback?.frontendPaths.length);
|
|
200
|
+
const useFallbackBackend = preferred.backendPaths.length === 0 && Boolean(fallback?.backendPaths.length);
|
|
201
|
+
return {
|
|
202
|
+
projectName: preferred.projectName ?? fallback?.projectName,
|
|
203
|
+
frontendPaths: preferred.frontendPaths.length > 0 ? preferred.frontendPaths : (fallback?.frontendPaths ?? []),
|
|
204
|
+
backendPaths: preferred.backendPaths.length > 0 ? preferred.backendPaths : (fallback?.backendPaths ?? []),
|
|
205
|
+
codeIndexStatus: preferred.codeIndexStatus ?? fallback?.codeIndexStatus,
|
|
206
|
+
sourcePath: [
|
|
207
|
+
preferred.sourcePath,
|
|
208
|
+
...((useFallbackFrontend || useFallbackBackend) && fallback ? [fallback.sourcePath] : []),
|
|
209
|
+
].join(", "),
|
|
210
|
+
};
|
|
106
211
|
}
|
|
107
212
|
|
|
108
213
|
export function executingState(label = "当前工作流步骤执行"): SessionControlState {
|
|
@@ -740,32 +845,106 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
740
845
|
}
|
|
741
846
|
|
|
742
847
|
async function collectProjectInitialization(ctx: {
|
|
848
|
+
cwd: string;
|
|
743
849
|
ui: {
|
|
850
|
+
select(title: string, options: Array<{ label: string; description?: string }>): Promise<string | undefined>;
|
|
744
851
|
input(title: string, placeholder?: string): Promise<string | undefined>;
|
|
745
852
|
confirm(title: string, message: string): Promise<boolean>;
|
|
746
853
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
747
854
|
};
|
|
748
855
|
}): Promise<ProjectInitializationRequest | null> {
|
|
749
|
-
const
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
856
|
+
const existing = discoverExistingProjectInitialization(ctx.cwd);
|
|
857
|
+
const reuseExisting = "使用已记录的项目结构(推荐)";
|
|
858
|
+
const rebuildExisting = "使用已记录的项目结构,并重新扫描代码";
|
|
859
|
+
const manualEntry = "我需要手动指定项目目录";
|
|
860
|
+
|
|
861
|
+
let projectName: string | undefined;
|
|
862
|
+
let frontendPath: string | undefined;
|
|
863
|
+
let backendPath: string | undefined;
|
|
864
|
+
let buildIndex: string | undefined;
|
|
865
|
+
let boundarySource: string | undefined;
|
|
866
|
+
|
|
867
|
+
if (existing) {
|
|
868
|
+
const frontend = existing.frontendPaths.length > 0 ? existing.frontendPaths.join(", ") : "未记录(unknown)";
|
|
869
|
+
const backend = existing.backendPaths.length > 0 ? existing.backendPaths.join(", ") : "未记录(unknown)";
|
|
870
|
+
const selected = await ctx.ui.select("检测到已有项目记录:这次初始化怎么处理?", [
|
|
871
|
+
{
|
|
872
|
+
label: reuseExisting,
|
|
873
|
+
description: `直接复用下面已核验、仍存在的目录,不重新扫描代码。\n前端:${frontend}\n后端:${backend}\n记录来源:${existing.sourcePath}${existing.codeIndexStatus ? `;索引状态:${existing.codeIndexStatus}` : ""}`,
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
label: rebuildExisting,
|
|
877
|
+
description: `沿用已登记目录,但重新扫描代码并更新索引。\n前端:${frontend}\n后端:${backend}`,
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
label: manualEntry,
|
|
881
|
+
description: "不采用上面的记录。我会依次询问项目名、前端目录和后端目录;目录均相对当前项目根目录。",
|
|
882
|
+
},
|
|
883
|
+
]);
|
|
884
|
+
if (!selected) return null;
|
|
885
|
+
if (selected === reuseExisting || selected === rebuildExisting) {
|
|
886
|
+
projectName = existing.projectName;
|
|
887
|
+
frontendPath = existing.frontendPaths.join(", ") || "unknown";
|
|
888
|
+
backendPath = existing.backendPaths.join(", ") || "unknown";
|
|
889
|
+
buildIndex = selected === reuseExisting
|
|
890
|
+
? `复用已有索引${existing.codeIndexStatus ? `(${existing.codeIndexStatus})` : ""}`
|
|
891
|
+
: "yes";
|
|
892
|
+
boundarySource = `${existing.sourcePath}(已校验目录仍存在)`;
|
|
893
|
+
}
|
|
762
894
|
}
|
|
895
|
+
|
|
896
|
+
if (!frontendPath || !backendPath || !buildIndex) {
|
|
897
|
+
projectName = await ctx.ui.input(
|
|
898
|
+
"项目显示名称(可留空)",
|
|
899
|
+
"用于 Devflow 文档标题;留空时使用当前目录名。例如:商城运营平台",
|
|
900
|
+
);
|
|
901
|
+
if (projectName === undefined) return null;
|
|
902
|
+
frontendPath = await ctx.ui.input(
|
|
903
|
+
"前端代码目录(相对当前项目根目录)",
|
|
904
|
+
"可填写多个目录,用英文逗号分隔。例:apps/admin, apps/h5;不确定请填 unknown",
|
|
905
|
+
);
|
|
906
|
+
if (frontendPath === undefined) return null;
|
|
907
|
+
backendPath = await ctx.ui.input(
|
|
908
|
+
"后端代码目录(相对当前项目根目录)",
|
|
909
|
+
"例:services/api 或 server;项目没有后端请填 none;不确定请填 unknown",
|
|
910
|
+
);
|
|
911
|
+
if (backendPath === undefined) return null;
|
|
912
|
+
const fullIndex = "建立完整代码索引(推荐)";
|
|
913
|
+
const skipIndex = "暂不建立代码索引";
|
|
914
|
+
const selectedIndex = await ctx.ui.select("是否扫描代码并建立索引?", [
|
|
915
|
+
{
|
|
916
|
+
label: fullIndex,
|
|
917
|
+
description: "扫描已确认的代码目录,建立调用关系和符号索引;后续需求与架构评估可直接复用。",
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
label: skipIndex,
|
|
921
|
+
description: "只建立项目说明与规则入口;后续需要源码级分析时再补建索引。",
|
|
922
|
+
},
|
|
923
|
+
]);
|
|
924
|
+
if (!selectedIndex) return null;
|
|
925
|
+
buildIndex = selectedIndex === fullIndex ? "yes" : "no";
|
|
926
|
+
}
|
|
927
|
+
const planMode = "只读规划(不写任何项目文件)";
|
|
928
|
+
const applyMode = "写入初始化";
|
|
929
|
+
const selectedMode = await ctx.ui.select("确认这次初始化是否写入项目文件?", [
|
|
930
|
+
{
|
|
931
|
+
label: planMode,
|
|
932
|
+
description: "只读取项目和已有初始化记录,输出规划与待确认项;不写入任何项目文件。",
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
label: applyMode,
|
|
936
|
+
description: "确认范围后创建或更新 Devflow 初始化产物;不修改业务源码、依赖或项目配置。",
|
|
937
|
+
},
|
|
938
|
+
]);
|
|
939
|
+
if (!selectedMode) return null;
|
|
940
|
+
const mode: ProjectInitializationMode = selectedMode === applyMode ? "apply" : "plan";
|
|
763
941
|
const modeSummary = mode === "apply" ? "写入初始化" : "只读规划";
|
|
764
942
|
const summary = [
|
|
765
943
|
`项目名:${projectName?.trim() || "当前目录"}`,
|
|
766
944
|
`前端:${frontendPath.trim() || "未提供"}`,
|
|
767
945
|
`后端:${backendPath.trim() || "未提供"}`,
|
|
768
946
|
`建立代码索引:${buildIndex.trim() || "未提供"}`,
|
|
947
|
+
...(boundarySource ? [`边界来源:${boundarySource}`] : []),
|
|
769
948
|
`初始化方式:${modeSummary}`,
|
|
770
949
|
].join("\n");
|
|
771
950
|
const consequence = mode === "apply"
|