Merged via squash. Prepared head SHA: d9a2869ad69db9449336a2e2846bd9de0e647ac6 Co-authored-by: maweibin <18023423+maweibin@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Slack/DM typing feedback: add `channels.slack.typingReaction` so Socket Mode DMs can show reaction-based processing status even when Slack native assistant typing is unavailable. (#19816) Thanks @dalefrieswthat.
|
- Slack/DM typing feedback: add `channels.slack.typingReaction` so Socket Mode DMs can show reaction-based processing status even when Slack native assistant typing is unavailable. (#19816) Thanks @dalefrieswthat.
|
||||||
- Cron/job snapshot persistence: skip backup during normalization persistence in `ensureLoaded` so `jobs.json.bak` keeps the pre-edit snapshot for recovery, while preserving backup creation on explicit user-driven writes. (#35234) Thanks @0xsline.
|
- Cron/job snapshot persistence: skip backup during normalization persistence in `ensureLoaded` so `jobs.json.bak` keeps the pre-edit snapshot for recovery, while preserving backup creation on explicit user-driven writes. (#35234) Thanks @0xsline.
|
||||||
- TTS/OpenAI-compatible endpoints: add `messages.tts.openai.baseUrl` config support with config-over-env precedence, endpoint-aware directive validation, and OpenAI TTS request routing to the resolved base URL. (#34321) thanks @RealKai42.
|
- TTS/OpenAI-compatible endpoints: add `messages.tts.openai.baseUrl` config support with config-over-env precedence, endpoint-aware directive validation, and OpenAI TTS request routing to the resolved base URL. (#34321) thanks @RealKai42.
|
||||||
|
- Plugins/before_prompt_build system-context fields: add `prependSystemContext` and `appendSystemContext` so static plugin guidance can be placed in system prompt space for provider caching and lower repeated prompt token cost. (#35177) thanks @maweibin.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ See [Hooks](/automation/hooks) for setup and examples.
|
|||||||
These run inside the agent loop or gateway pipeline:
|
These run inside the agent loop or gateway pipeline:
|
||||||
|
|
||||||
- **`before_model_resolve`**: runs pre-session (no `messages`) to deterministically override provider/model before model resolution.
|
- **`before_model_resolve`**: runs pre-session (no `messages`) to deterministically override provider/model before model resolution.
|
||||||
- **`before_prompt_build`**: runs after session load (with `messages`) to inject `prependContext`/`systemPrompt` before prompt submission.
|
- **`before_prompt_build`**: runs after session load (with `messages`) to inject `prependContext`, `systemPrompt`, `prependSystemContext`, or `appendSystemContext` before prompt submission. Use `prependContext` for per-turn dynamic text and system-context fields for stable guidance that should sit in system prompt space.
|
||||||
- **`before_agent_start`**: legacy compatibility hook that may run in either phase; prefer the explicit hooks above.
|
- **`before_agent_start`**: legacy compatibility hook that may run in either phase; prefer the explicit hooks above.
|
||||||
- **`agent_end`**: inspect the final message list and run metadata after completion.
|
- **`agent_end`**: inspect the final message list and run metadata after completion.
|
||||||
- **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles.
|
- **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles.
|
||||||
|
|||||||
@@ -431,6 +431,54 @@ Notes:
|
|||||||
- Plugin-managed hooks show up in `openclaw hooks list` with `plugin:<id>`.
|
- Plugin-managed hooks show up in `openclaw hooks list` with `plugin:<id>`.
|
||||||
- You cannot enable/disable plugin-managed hooks via `openclaw hooks`; enable/disable the plugin instead.
|
- You cannot enable/disable plugin-managed hooks via `openclaw hooks`; enable/disable the plugin instead.
|
||||||
|
|
||||||
|
### Agent lifecycle hooks (`api.on`)
|
||||||
|
|
||||||
|
For typed runtime lifecycle hooks, use `api.on(...)`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default function register(api) {
|
||||||
|
api.on(
|
||||||
|
"before_prompt_build",
|
||||||
|
(event, ctx) => {
|
||||||
|
return {
|
||||||
|
prependSystemContext: "Follow company style guide.",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ priority: 10 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Important hooks for prompt construction:
|
||||||
|
|
||||||
|
- `before_model_resolve`: runs before session load (`messages` are not available). Use this to deterministically override `modelOverride` or `providerOverride`.
|
||||||
|
- `before_prompt_build`: runs after session load (`messages` are available). Use this to shape prompt input.
|
||||||
|
- `before_agent_start`: legacy compatibility hook. Prefer the two explicit hooks above.
|
||||||
|
|
||||||
|
`before_prompt_build` result fields:
|
||||||
|
|
||||||
|
- `prependContext`: prepends text to the user prompt for this run. Best for turn-specific or dynamic content.
|
||||||
|
- `systemPrompt`: full system prompt override.
|
||||||
|
- `prependSystemContext`: prepends text to the current system prompt.
|
||||||
|
- `appendSystemContext`: appends text to the current system prompt.
|
||||||
|
|
||||||
|
Prompt build order in embedded runtime:
|
||||||
|
|
||||||
|
1. Apply `prependContext` to the user prompt.
|
||||||
|
2. Apply `systemPrompt` override when provided.
|
||||||
|
3. Apply `prependSystemContext + current system prompt + appendSystemContext`.
|
||||||
|
|
||||||
|
Merge and precedence notes:
|
||||||
|
|
||||||
|
- Hook handlers run by priority (higher first).
|
||||||
|
- For merged context fields, values are concatenated in execution order.
|
||||||
|
- `before_prompt_build` values are applied before legacy `before_agent_start` fallback values.
|
||||||
|
|
||||||
|
Migration guidance:
|
||||||
|
|
||||||
|
- Move static guidance from `prependContext` to `prependSystemContext` (or `appendSystemContext`) so providers can cache stable system-prefix content.
|
||||||
|
- Keep `prependContext` for per-turn dynamic context that should stay tied to the user message.
|
||||||
|
|
||||||
## Provider plugins (model auth)
|
## Provider plugins (model auth)
|
||||||
|
|
||||||
Plugins can register **model provider auth** flows so users can run OAuth or
|
Plugins can register **model provider auth** flows so users can run OAuth or
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import type { OpenClawConfig } from "../../../config/config.js";
|
import type { OpenClawConfig } from "../../../config/config.js";
|
||||||
import {
|
import {
|
||||||
|
composeSystemPromptWithHookContext,
|
||||||
isOllamaCompatProvider,
|
isOllamaCompatProvider,
|
||||||
resolveAttemptFsWorkspaceOnly,
|
resolveAttemptFsWorkspaceOnly,
|
||||||
resolveOllamaBaseUrlForRun,
|
resolveOllamaBaseUrlForRun,
|
||||||
@@ -54,6 +55,8 @@ describe("resolvePromptBuildHookResult", () => {
|
|||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
prependContext: "from-cache",
|
prependContext: "from-cache",
|
||||||
systemPrompt: "legacy-system",
|
systemPrompt: "legacy-system",
|
||||||
|
prependSystemContext: undefined,
|
||||||
|
appendSystemContext: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,6 +74,58 @@ describe("resolvePromptBuildHookResult", () => {
|
|||||||
expect(hookRunner.runBeforeAgentStart).toHaveBeenCalledWith({ prompt: "hello", messages }, {});
|
expect(hookRunner.runBeforeAgentStart).toHaveBeenCalledWith({ prompt: "hello", messages }, {});
|
||||||
expect(result.prependContext).toBe("from-hook");
|
expect(result.prependContext).toBe("from-hook");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("merges prompt-build and legacy context fields in deterministic order", async () => {
|
||||||
|
const hookRunner = {
|
||||||
|
hasHooks: vi.fn(() => true),
|
||||||
|
runBeforePromptBuild: vi.fn(async () => ({
|
||||||
|
prependContext: "prompt context",
|
||||||
|
prependSystemContext: "prompt prepend",
|
||||||
|
appendSystemContext: "prompt append",
|
||||||
|
})),
|
||||||
|
runBeforeAgentStart: vi.fn(async () => ({
|
||||||
|
prependContext: "legacy context",
|
||||||
|
prependSystemContext: "legacy prepend",
|
||||||
|
appendSystemContext: "legacy append",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await resolvePromptBuildHookResult({
|
||||||
|
prompt: "hello",
|
||||||
|
messages: [],
|
||||||
|
hookCtx: {},
|
||||||
|
hookRunner,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.prependContext).toBe("prompt context\n\nlegacy context");
|
||||||
|
expect(result.prependSystemContext).toBe("prompt prepend\n\nlegacy prepend");
|
||||||
|
expect(result.appendSystemContext).toBe("prompt append\n\nlegacy append");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("composeSystemPromptWithHookContext", () => {
|
||||||
|
it("returns undefined when no hook system context is provided", () => {
|
||||||
|
expect(composeSystemPromptWithHookContext({ baseSystemPrompt: "base" })).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds prepend/base/append system prompt order", () => {
|
||||||
|
expect(
|
||||||
|
composeSystemPromptWithHookContext({
|
||||||
|
baseSystemPrompt: " base system ",
|
||||||
|
prependSystemContext: " prepend ",
|
||||||
|
appendSystemContext: " append ",
|
||||||
|
}),
|
||||||
|
).toBe("prepend\n\nbase system\n\nappend");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("avoids blank separators when base system prompt is empty", () => {
|
||||||
|
expect(
|
||||||
|
composeSystemPromptWithHookContext({
|
||||||
|
baseSystemPrompt: " ",
|
||||||
|
appendSystemContext: " append only ",
|
||||||
|
}),
|
||||||
|
).toBe("append only");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolvePromptModeForSession", () => {
|
describe("resolvePromptModeForSession", () => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
PluginHookBeforePromptBuildResult,
|
PluginHookBeforePromptBuildResult,
|
||||||
} from "../../../plugins/types.js";
|
} from "../../../plugins/types.js";
|
||||||
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
||||||
|
import { joinPresentTextSegments } from "../../../shared/text/join-segments.js";
|
||||||
import { resolveSignalReactionLevel } from "../../../signal/reaction-level.js";
|
import { resolveSignalReactionLevel } from "../../../signal/reaction-level.js";
|
||||||
import { resolveTelegramInlineButtonsScope } from "../../../telegram/inline-buttons.js";
|
import { resolveTelegramInlineButtonsScope } from "../../../telegram/inline-buttons.js";
|
||||||
import { resolveTelegramReactionLevel } from "../../../telegram/reaction-level.js";
|
import { resolveTelegramReactionLevel } from "../../../telegram/reaction-level.js";
|
||||||
@@ -567,12 +568,37 @@ export async function resolvePromptBuildHookResult(params: {
|
|||||||
: undefined);
|
: undefined);
|
||||||
return {
|
return {
|
||||||
systemPrompt: promptBuildResult?.systemPrompt ?? legacyResult?.systemPrompt,
|
systemPrompt: promptBuildResult?.systemPrompt ?? legacyResult?.systemPrompt,
|
||||||
prependContext: [promptBuildResult?.prependContext, legacyResult?.prependContext]
|
prependContext: joinPresentTextSegments([
|
||||||
.filter((value): value is string => Boolean(value))
|
promptBuildResult?.prependContext,
|
||||||
.join("\n\n"),
|
legacyResult?.prependContext,
|
||||||
|
]),
|
||||||
|
prependSystemContext: joinPresentTextSegments([
|
||||||
|
promptBuildResult?.prependSystemContext,
|
||||||
|
legacyResult?.prependSystemContext,
|
||||||
|
]),
|
||||||
|
appendSystemContext: joinPresentTextSegments([
|
||||||
|
promptBuildResult?.appendSystemContext,
|
||||||
|
legacyResult?.appendSystemContext,
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function composeSystemPromptWithHookContext(params: {
|
||||||
|
baseSystemPrompt?: string;
|
||||||
|
prependSystemContext?: string;
|
||||||
|
appendSystemContext?: string;
|
||||||
|
}): string | undefined {
|
||||||
|
const prependSystem = params.prependSystemContext?.trim();
|
||||||
|
const appendSystem = params.appendSystemContext?.trim();
|
||||||
|
if (!prependSystem && !appendSystem) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return joinPresentTextSegments(
|
||||||
|
[params.prependSystemContext, params.baseSystemPrompt, params.appendSystemContext],
|
||||||
|
{ trim: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function resolvePromptModeForSession(sessionKey?: string): "minimal" | "full" {
|
export function resolvePromptModeForSession(sessionKey?: string): "minimal" | "full" {
|
||||||
if (!sessionKey) {
|
if (!sessionKey) {
|
||||||
return "full";
|
return "full";
|
||||||
@@ -1522,6 +1548,20 @@ export async function runEmbeddedAttempt(
|
|||||||
systemPromptText = legacySystemPrompt;
|
systemPromptText = legacySystemPrompt;
|
||||||
log.debug(`hooks: applied systemPrompt override (${legacySystemPrompt.length} chars)`);
|
log.debug(`hooks: applied systemPrompt override (${legacySystemPrompt.length} chars)`);
|
||||||
}
|
}
|
||||||
|
const prependedOrAppendedSystemPrompt = composeSystemPromptWithHookContext({
|
||||||
|
baseSystemPrompt: systemPromptText,
|
||||||
|
prependSystemContext: hookResult?.prependSystemContext,
|
||||||
|
appendSystemContext: hookResult?.appendSystemContext,
|
||||||
|
});
|
||||||
|
if (prependedOrAppendedSystemPrompt) {
|
||||||
|
const prependSystemLen = hookResult?.prependSystemContext?.trim().length ?? 0;
|
||||||
|
const appendSystemLen = hookResult?.appendSystemContext?.trim().length ?? 0;
|
||||||
|
applySystemPromptOverrideToSession(activeSession, prependedOrAppendedSystemPrompt);
|
||||||
|
systemPromptText = prependedOrAppendedSystemPrompt;
|
||||||
|
log.debug(
|
||||||
|
`hooks: applied prependSystemContext/appendSystemContext (${prependSystemLen}+${appendSystemLen} chars)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(`embedded run prompt start: runId=${params.runId} sessionId=${params.sessionId}`);
|
log.debug(`embedded run prompt start: runId=${params.runId} sessionId=${params.sessionId}`);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* 3. before_agent_start remains a legacy compatibility fallback
|
* 3. before_agent_start remains a legacy compatibility fallback
|
||||||
*/
|
*/
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { joinPresentTextSegments } from "../shared/text/join-segments.js";
|
||||||
import { createHookRunner } from "./hooks.js";
|
import { createHookRunner } from "./hooks.js";
|
||||||
import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js";
|
import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js";
|
||||||
import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js";
|
import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js";
|
||||||
@@ -154,9 +155,10 @@ describe("model override pipeline wiring", () => {
|
|||||||
{ prompt: "test", messages: [{ role: "user", content: "x" }] as unknown[] },
|
{ prompt: "test", messages: [{ role: "user", content: "x" }] as unknown[] },
|
||||||
stubCtx,
|
stubCtx,
|
||||||
);
|
);
|
||||||
const prependContext = [promptBuild?.prependContext, legacy?.prependContext]
|
const prependContext = joinPresentTextSegments([
|
||||||
.filter((value): value is string => Boolean(value))
|
promptBuild?.prependContext,
|
||||||
.join("\n\n");
|
legacy?.prependContext,
|
||||||
|
]);
|
||||||
|
|
||||||
expect(prependContext).toBe("new context\n\nlegacy context");
|
expect(prependContext).toBe("new context\n\nlegacy context");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,4 +72,33 @@ describe("phase hooks merger", () => {
|
|||||||
expect(result?.prependContext).toBe("context A\n\ncontext B");
|
expect(result?.prependContext).toBe("context A\n\ncontext B");
|
||||||
expect(result?.systemPrompt).toBe("system A");
|
expect(result?.systemPrompt).toBe("system A");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("before_prompt_build concatenates prependSystemContext and appendSystemContext", async () => {
|
||||||
|
addTypedHook(
|
||||||
|
registry,
|
||||||
|
"before_prompt_build",
|
||||||
|
"first",
|
||||||
|
() => ({
|
||||||
|
prependSystemContext: "prepend A",
|
||||||
|
appendSystemContext: "append A",
|
||||||
|
}),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
addTypedHook(
|
||||||
|
registry,
|
||||||
|
"before_prompt_build",
|
||||||
|
"second",
|
||||||
|
() => ({
|
||||||
|
prependSystemContext: "prepend B",
|
||||||
|
appendSystemContext: "append B",
|
||||||
|
}),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
const runner = createHookRunner(registry);
|
||||||
|
const result = await runner.runBeforePromptBuild({ prompt: "test", messages: [] }, {});
|
||||||
|
|
||||||
|
expect(result?.prependSystemContext).toBe("prepend A\n\nprepend B");
|
||||||
|
expect(result?.appendSystemContext).toBe("append A\n\nappend B");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* error handling, priority ordering, and async support.
|
* error handling, priority ordering, and async support.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { concatOptionalTextSegments } from "../shared/text/join-segments.js";
|
||||||
import type { PluginRegistry } from "./registry.js";
|
import type { PluginRegistry } from "./registry.js";
|
||||||
import type {
|
import type {
|
||||||
PluginHookAfterCompactionEvent,
|
PluginHookAfterCompactionEvent,
|
||||||
@@ -140,10 +141,18 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
|
|||||||
next: PluginHookBeforePromptBuildResult,
|
next: PluginHookBeforePromptBuildResult,
|
||||||
): PluginHookBeforePromptBuildResult => ({
|
): PluginHookBeforePromptBuildResult => ({
|
||||||
systemPrompt: next.systemPrompt ?? acc?.systemPrompt,
|
systemPrompt: next.systemPrompt ?? acc?.systemPrompt,
|
||||||
prependContext:
|
prependContext: concatOptionalTextSegments({
|
||||||
acc?.prependContext && next.prependContext
|
left: acc?.prependContext,
|
||||||
? `${acc.prependContext}\n\n${next.prependContext}`
|
right: next.prependContext,
|
||||||
: (next.prependContext ?? acc?.prependContext),
|
}),
|
||||||
|
prependSystemContext: concatOptionalTextSegments({
|
||||||
|
left: acc?.prependSystemContext,
|
||||||
|
right: next.prependSystemContext,
|
||||||
|
}),
|
||||||
|
appendSystemContext: concatOptionalTextSegments({
|
||||||
|
left: acc?.appendSystemContext,
|
||||||
|
right: next.appendSystemContext,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergeSubagentSpawningResult = (
|
const mergeSubagentSpawningResult = (
|
||||||
|
|||||||
@@ -369,6 +369,16 @@ export type PluginHookBeforePromptBuildEvent = {
|
|||||||
export type PluginHookBeforePromptBuildResult = {
|
export type PluginHookBeforePromptBuildResult = {
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
prependContext?: string;
|
prependContext?: string;
|
||||||
|
/**
|
||||||
|
* Prepended to the agent system prompt so providers can cache it (e.g. prompt caching).
|
||||||
|
* Use for static plugin guidance instead of prependContext to avoid per-turn token cost.
|
||||||
|
*/
|
||||||
|
prependSystemContext?: string;
|
||||||
|
/**
|
||||||
|
* Appended to the agent system prompt so providers can cache it (e.g. prompt caching).
|
||||||
|
* Use for static plugin guidance instead of prependContext to avoid per-turn token cost.
|
||||||
|
*/
|
||||||
|
appendSystemContext?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// before_agent_start hook (legacy compatibility: combines both phases)
|
// before_agent_start hook (legacy compatibility: combines both phases)
|
||||||
|
|||||||
26
src/shared/text/join-segments.test.ts
Normal file
26
src/shared/text/join-segments.test.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { concatOptionalTextSegments, joinPresentTextSegments } from "./join-segments.js";
|
||||||
|
|
||||||
|
describe("concatOptionalTextSegments", () => {
|
||||||
|
it("concatenates left and right with default separator", () => {
|
||||||
|
expect(concatOptionalTextSegments({ left: "A", right: "B" })).toBe("A\n\nB");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps explicit empty-string right value", () => {
|
||||||
|
expect(concatOptionalTextSegments({ left: "A", right: "" })).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("joinPresentTextSegments", () => {
|
||||||
|
it("joins non-empty segments", () => {
|
||||||
|
expect(joinPresentTextSegments(["A", undefined, "B"])).toBe("A\n\nB");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when all segments are empty", () => {
|
||||||
|
expect(joinPresentTextSegments(["", undefined, null])).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trims segments when requested", () => {
|
||||||
|
expect(joinPresentTextSegments([" A ", " B "], { trim: true })).toBe("A\n\nB");
|
||||||
|
});
|
||||||
|
});
|
||||||
34
src/shared/text/join-segments.ts
Normal file
34
src/shared/text/join-segments.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export function concatOptionalTextSegments(params: {
|
||||||
|
left?: string;
|
||||||
|
right?: string;
|
||||||
|
separator?: string;
|
||||||
|
}): string | undefined {
|
||||||
|
const separator = params.separator ?? "\n\n";
|
||||||
|
if (params.left && params.right) {
|
||||||
|
return `${params.left}${separator}${params.right}`;
|
||||||
|
}
|
||||||
|
return params.right ?? params.left;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinPresentTextSegments(
|
||||||
|
segments: ReadonlyArray<string | null | undefined>,
|
||||||
|
options?: {
|
||||||
|
separator?: string;
|
||||||
|
trim?: boolean;
|
||||||
|
},
|
||||||
|
): string | undefined {
|
||||||
|
const separator = options?.separator ?? "\n\n";
|
||||||
|
const trim = options?.trim ?? false;
|
||||||
|
const values: string[] = [];
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (typeof segment !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalized = trim ? segment.trim() : segment;
|
||||||
|
if (!normalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
values.push(normalized);
|
||||||
|
}
|
||||||
|
return values.length > 0 ? values.join(separator) : undefined;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user