feat: 优化内容模板变量引用与换行展示

- 内容模板直接引用上游参数并自动维护运行参数

- 支持 Enjoy 变量标签编辑及中文变量显示

- 保留试运行与聊天运行页的模板输出换行
This commit is contained in:
2026-08-11 21:41:58 +08:00
parent fe41d62b8a
commit 765006747a
12 changed files with 419 additions and 67 deletions

View File

@@ -5,6 +5,8 @@ import {
findBackspaceTokenRange,
findTokenRangeAtCursor,
flattenParameterNames,
formatParamToken,
getTokenRanges,
insertTextAtCursor,
parseTokenParts,
splitTokenDisplay,
@@ -64,18 +66,21 @@ describe('paramToken utils', () => {
expect(result).toEqual([
{
dataType: undefined,
disconnected: false,
displayName: 'input',
name: 'input',
resolved: false,
},
{
dataType: undefined,
disconnected: false,
displayName: 'docs',
name: 'docs',
resolved: true,
},
{
dataType: undefined,
disconnected: false,
displayName: 'runtimeInput',
name: 'runtimeInput',
resolved: true,
@@ -178,4 +183,20 @@ describe('paramToken utils', () => {
expect(rightBoundary?.key).toBe('docs');
expect(leftBoundary?.key).toBe('docs');
});
it('should format and parse Enjoy parameter tokens', () => {
expect(formatParamToken('申请人', 'enjoy')).toBe('#(申请人)');
expect(getTokenRanges('申请:#(申请人)', 'enjoy')).toEqual([
{
start: 3,
end: 9,
text: '#(申请人)',
key: '申请人',
},
]);
});
it('should keep complex Enjoy expressions as editable text', () => {
expect(getTokenRanges('#(申请人 ?? 被申请人)', 'enjoy')).toEqual([]);
});
});

View File

@@ -37,7 +37,22 @@ export interface ParameterCandidate {
dataType?: string;
}
const TOKEN_PATTERN = /\{\{\s*([^{}]+?)\s*}}/g;
export type TokenSyntax = 'enjoy' | 'mustache';
const MUSTACHE_TOKEN_PATTERN = /\{\{\s*([^{}]+?)\s*}}/g;
const ENJOY_TOKEN_PATTERN =
/#\(\s*([A-Za-z_$\u4E00-\u9FA5][A-Za-z0-9_$\u4E00-\u9FA5]*(?:\.[A-Za-z_$\u4E00-\u9FA5][A-Za-z0-9_$\u4E00-\u9FA5]*)*)\s*\)/g;
function getTokenPattern(syntax: TokenSyntax): RegExp {
return syntax === 'enjoy' ? ENJOY_TOKEN_PATTERN : MUSTACHE_TOKEN_PATTERN;
}
export function formatParamToken(
tokenKey: string,
syntax: TokenSyntax = 'mustache',
): string {
return syntax === 'enjoy' ? `#(${tokenKey})` : `{{${tokenKey}}}`;
}
export function normalizeTokenKey(tokenKey: string): string {
return tokenKey.trim();
@@ -156,14 +171,16 @@ export function flattenParameterCandidates(
export function parseTokenParts(
content: string,
validParams: string[] = [],
syntax: TokenSyntax = 'mustache',
): TokenPart[] {
const source = content ?? '';
const validSet = new Set(validParams.map(normalizeTokenKey));
const parts: TokenPart[] = [];
const tokenPattern = getTokenPattern(syntax);
let lastIndex = 0;
TOKEN_PATTERN.lastIndex = 0;
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
tokenPattern.lastIndex = 0;
let match: RegExpExecArray | null = tokenPattern.exec(source);
while (match) {
if (match.index > lastIndex) {
@@ -183,7 +200,7 @@ export function parseTokenParts(
});
lastIndex = match.index + rawToken.length;
match = TOKEN_PATTERN.exec(source);
match = tokenPattern.exec(source);
}
if (lastIndex < source.length) {
@@ -203,11 +220,15 @@ export function parseTokenParts(
return parts;
}
export function getTokenRanges(content: string): TokenRange[] {
export function getTokenRanges(
content: string,
syntax: TokenSyntax = 'mustache',
): TokenRange[] {
const source = content ?? '';
const ranges: TokenRange[] = [];
TOKEN_PATTERN.lastIndex = 0;
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
const tokenPattern = getTokenPattern(syntax);
tokenPattern.lastIndex = 0;
let match: RegExpExecArray | null = tokenPattern.exec(source);
while (match) {
const rawToken = match[0];
@@ -217,7 +238,7 @@ export function getTokenRanges(content: string): TokenRange[] {
text: rawToken,
key: normalizeTokenKey(match[1] || ''),
});
match = TOKEN_PATTERN.exec(source);
match = tokenPattern.exec(source);
}
return ranges;
@@ -229,6 +250,7 @@ export function findTokenRangeAtCursor(
options?: {
includeStart?: boolean;
includeEnd?: boolean;
syntax?: TokenSyntax;
},
): TokenRange | null {
if (!Number.isInteger(cursor) || cursor < 0) {
@@ -237,7 +259,7 @@ export function findTokenRangeAtCursor(
const includeStart = options?.includeStart ?? false;
const includeEnd = options?.includeEnd ?? false;
const ranges = getTokenRanges(content);
const ranges = getTokenRanges(content, options?.syntax);
for (const range of ranges) {
const leftValid = includeStart
? cursor >= range.start
@@ -254,10 +276,12 @@ export function findTokenRangeAtCursor(
export function findBackspaceTokenRange(
content: string,
cursor: number,
syntax: TokenSyntax = 'mustache',
): TokenRange | null {
return findTokenRangeAtCursor(content, cursor, {
includeStart: false,
includeEnd: true,
syntax,
});
}