支持reasoning
This commit is contained in:
@@ -92,56 +92,25 @@ curl http://127.0.0.1:18000/v1/chat/completions \
|
||||
<!-- gitnexus:start -->
|
||||
# GitNexus — Code Intelligence
|
||||
|
||||
This project is indexed by GitNexus as **command-code-openai-bridge** (106 symbols, 201 relationships, 9 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
This project is indexed by GitNexus as **command-code-openai-bridge** (428 symbols, 1186 relationships, 33 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
||||
|
||||
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
|
||||
> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939).
|
||||
|
||||
## Always Do
|
||||
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
||||
- **MUST run `detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: `detect_changes({scope: "compare", base_ref: "main"})`.
|
||||
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
||||
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
|
||||
|
||||
## When Debugging
|
||||
|
||||
1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
|
||||
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
|
||||
3. `READ gitnexus://repo/command-code-openai-bridge/process/{processName}` — trace the full execution flow step by step
|
||||
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed
|
||||
|
||||
## When Refactoring
|
||||
|
||||
- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
|
||||
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
|
||||
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.
|
||||
- When exploring unfamiliar code, use `query({search_query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
||||
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`.
|
||||
- For security review, `explain({target: "fileOrSymbol"})` lists taint findings (source→sink flows; needs `analyze --pdg`).
|
||||
|
||||
## Never Do
|
||||
|
||||
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
|
||||
- NEVER edit a function, class, or method without first running `impact` on it.
|
||||
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
||||
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
|
||||
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
|
||||
|
||||
## Tools Quick Reference
|
||||
|
||||
| Tool | When to use | Command |
|
||||
|------|-------------|---------|
|
||||
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
|
||||
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
|
||||
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
|
||||
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
|
||||
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
|
||||
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |
|
||||
|
||||
## Impact Risk Levels
|
||||
|
||||
| Depth | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
|
||||
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
|
||||
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
|
||||
- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph.
|
||||
- NEVER commit changes without running `detect_changes()` to check affected scope.
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -152,32 +121,6 @@ This project is indexed by GitNexus as **command-code-openai-bridge** (106 symbo
|
||||
| `gitnexus://repo/command-code-openai-bridge/processes` | All execution flows |
|
||||
| `gitnexus://repo/command-code-openai-bridge/process/{name}` | Step-by-step execution trace |
|
||||
|
||||
## Self-Check Before Finishing
|
||||
|
||||
Before completing any code modification task, verify:
|
||||
1. `gitnexus_impact` was run for all modified symbols
|
||||
2. No HIGH/CRITICAL risk warnings were ignored
|
||||
3. `gitnexus_detect_changes()` confirms changes match expected scope
|
||||
4. All d=1 (WILL BREAK) dependents were updated
|
||||
|
||||
## Keeping the Index Fresh
|
||||
|
||||
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze
|
||||
```
|
||||
|
||||
If the index previously included embeddings, preserve them by adding `--embeddings`:
|
||||
|
||||
```bash
|
||||
npx gitnexus analyze --embeddings
|
||||
```
|
||||
|
||||
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
|
||||
|
||||
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
|
||||
|
||||
## CLI
|
||||
|
||||
| Task | Read this skill file |
|
||||
|
||||
+197
-22
@@ -227,7 +227,20 @@ interface ResponseOutputFunctionCall {
|
||||
status: "completed" | "incomplete";
|
||||
}
|
||||
|
||||
type ResponseOutputItem = ResponseOutputMessage | ResponseOutputFunctionCall;
|
||||
interface ResponseReasoningSummaryText {
|
||||
type: "summary_text";
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ResponseOutputReasoning {
|
||||
id: string;
|
||||
type: "reasoning";
|
||||
summary: ResponseReasoningSummaryText[];
|
||||
status: "in_progress" | "completed" | "incomplete";
|
||||
content?: Array<{ type: "reasoning_text"; text: string }>;
|
||||
}
|
||||
|
||||
type ResponseOutputItem = ResponseOutputMessage | ResponseOutputFunctionCall | ResponseOutputReasoning;
|
||||
|
||||
interface ResponseUsage {
|
||||
input_tokens: number;
|
||||
@@ -285,11 +298,13 @@ interface StructuredValidation {
|
||||
interface AttemptResult {
|
||||
result: CommandResult;
|
||||
finalChunks: string[];
|
||||
reasoningItems: ResponseOutputReasoning[];
|
||||
}
|
||||
|
||||
interface ExecutionResult {
|
||||
finalText: string;
|
||||
chunks: string[];
|
||||
reasoningItems: ResponseOutputReasoning[];
|
||||
usage?: CommandUsage;
|
||||
validationErrors?: string[];
|
||||
toolCalls?: FunctionToolCall[];
|
||||
@@ -411,10 +426,72 @@ class ResponseStore {
|
||||
}
|
||||
}
|
||||
|
||||
class ResponseReasoningAccumulator {
|
||||
private readonly completed: ResponseOutputReasoning[] = [];
|
||||
private current: { id: string; outputIndex: number; text: string } | undefined;
|
||||
|
||||
constructor(private readonly writer?: ResponsesSseWriter) {}
|
||||
|
||||
event(event: Record<string, unknown>): void {
|
||||
if (event.type === "thinking_start") {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
if (event.type === "thinking_delta" && typeof event.delta === "string") {
|
||||
this.add(event.delta);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.type === "thinking_end"
|
||||
|| event.type === "turn_start"
|
||||
|| event.type === "text_delta"
|
||||
|| event.type === "message_end"
|
||||
|| event.type === "turn_end"
|
||||
) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
finish(): ResponseOutputReasoning[] {
|
||||
this.close();
|
||||
return [...this.completed];
|
||||
}
|
||||
|
||||
private add(delta: string): void {
|
||||
if (delta === "") return;
|
||||
if (!this.current) {
|
||||
this.current = {
|
||||
id: newReasoningItemId(),
|
||||
outputIndex: this.completed.length,
|
||||
text: "",
|
||||
};
|
||||
this.writer?.beginReasoning(this.current.id, this.current.outputIndex);
|
||||
}
|
||||
this.current.text += delta;
|
||||
this.writer?.addReasoningDelta(this.current.id, this.current.outputIndex, delta);
|
||||
}
|
||||
|
||||
private close(): void {
|
||||
if (!this.current) return;
|
||||
const item: ResponseOutputReasoning = {
|
||||
id: this.current.id,
|
||||
type: "reasoning",
|
||||
summary: [{ type: "summary_text", text: this.current.text }],
|
||||
status: "completed",
|
||||
};
|
||||
this.completed.push(item);
|
||||
this.writer?.finishReasoning(item, this.current.outputIndex);
|
||||
this.current = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class ResponsesSseWriter {
|
||||
private sequenceNumber = 0;
|
||||
private outputStarted = false;
|
||||
private outputText = "";
|
||||
private textOutputIndex: number | undefined;
|
||||
private readonly emittedReasoningIds = new Set<string>();
|
||||
private readonly completedReasoning = new Map<string, ResponseOutputReasoning>();
|
||||
|
||||
constructor(
|
||||
private readonly response: ServerResponse,
|
||||
@@ -432,11 +509,72 @@ class ResponsesSseWriter {
|
||||
this.event("response.in_progress", { response: initial });
|
||||
}
|
||||
|
||||
beginReasoning(itemId: string, outputIndex: number): void {
|
||||
if (this.emittedReasoningIds.has(itemId)) return;
|
||||
this.emittedReasoningIds.add(itemId);
|
||||
this.event("response.output_item.added", {
|
||||
output_index: outputIndex,
|
||||
item: {
|
||||
id: itemId,
|
||||
type: "reasoning",
|
||||
summary: [],
|
||||
status: "in_progress",
|
||||
},
|
||||
});
|
||||
this.event("response.reasoning_summary_part.added", {
|
||||
item_id: itemId,
|
||||
output_index: outputIndex,
|
||||
summary_index: 0,
|
||||
part: { type: "summary_text", text: "" },
|
||||
});
|
||||
}
|
||||
|
||||
addReasoningDelta(itemId: string, outputIndex: number, delta: string): void {
|
||||
if (delta === "" || !this.emittedReasoningIds.has(itemId)) return;
|
||||
this.event("response.reasoning_summary_text.delta", {
|
||||
item_id: itemId,
|
||||
output_index: outputIndex,
|
||||
summary_index: 0,
|
||||
delta,
|
||||
});
|
||||
}
|
||||
|
||||
finishReasoning(item: ResponseOutputReasoning, outputIndex: number): void {
|
||||
if (this.completedReasoning.has(item.id)) return;
|
||||
const part = item.summary[0];
|
||||
if (!part) return;
|
||||
this.completedReasoning.set(item.id, item);
|
||||
this.event("response.reasoning_summary_text.done", {
|
||||
item_id: item.id,
|
||||
output_index: outputIndex,
|
||||
summary_index: 0,
|
||||
text: part.text,
|
||||
});
|
||||
this.event("response.reasoning_summary_part.done", {
|
||||
item_id: item.id,
|
||||
output_index: outputIndex,
|
||||
summary_index: 0,
|
||||
part,
|
||||
});
|
||||
this.event("response.output_item.done", { output_index: outputIndex, item });
|
||||
}
|
||||
|
||||
addReasoningItems(items: ResponseOutputReasoning[]): void {
|
||||
for (const [outputIndex, item] of items.entries()) {
|
||||
if (this.emittedReasoningIds.has(item.id)) continue;
|
||||
this.beginReasoning(item.id, outputIndex);
|
||||
const part = item.summary[0];
|
||||
if (part?.text) this.addReasoningDelta(item.id, outputIndex, part.text);
|
||||
this.finishReasoning(item, outputIndex);
|
||||
}
|
||||
}
|
||||
|
||||
addText(chunks: string[]): void {
|
||||
if (!this.outputStarted) {
|
||||
this.outputStarted = true;
|
||||
this.textOutputIndex = this.completedReasoning.size;
|
||||
this.event("response.output_item.added", {
|
||||
output_index: 0,
|
||||
output_index: this.textOutputIndex,
|
||||
item: {
|
||||
id: this.messageId,
|
||||
type: "message",
|
||||
@@ -447,7 +585,7 @@ class ResponsesSseWriter {
|
||||
});
|
||||
this.event("response.content_part.added", {
|
||||
item_id: this.messageId,
|
||||
output_index: 0,
|
||||
output_index: this.textOutputIndex,
|
||||
content_index: 0,
|
||||
part: { type: "output_text", text: "", annotations: [] },
|
||||
});
|
||||
@@ -458,7 +596,7 @@ class ResponsesSseWriter {
|
||||
this.outputText += delta;
|
||||
this.event("response.output_text.delta", {
|
||||
item_id: this.messageId,
|
||||
output_index: 0,
|
||||
output_index: this.textOutputIndex,
|
||||
content_index: 0,
|
||||
delta,
|
||||
});
|
||||
@@ -471,28 +609,38 @@ class ResponsesSseWriter {
|
||||
this.event(terminalEvent, { response });
|
||||
return;
|
||||
}
|
||||
const outputIndex = response.output.findIndex((item) => (
|
||||
item.type === "message" && item.id === this.messageId
|
||||
));
|
||||
if (outputIndex < 0) {
|
||||
this.event(terminalEvent, { response });
|
||||
return;
|
||||
}
|
||||
this.ensureFinalText(response.output_text);
|
||||
if (this.outputStarted) {
|
||||
const outputMessage = response.output[0];
|
||||
const outputMessage = response.output[outputIndex];
|
||||
if (!outputMessage || outputMessage.type !== "message") {
|
||||
throw new Error("Response output message is missing");
|
||||
}
|
||||
if (this.textOutputIndex !== outputIndex) {
|
||||
throw new Error("Response output message index does not match the streamed item index");
|
||||
}
|
||||
const part = outputMessage.content[0];
|
||||
if (!part) throw new Error("Response output text part is missing");
|
||||
|
||||
this.event("response.output_text.done", {
|
||||
item_id: this.messageId,
|
||||
output_index: 0,
|
||||
output_index: outputIndex,
|
||||
content_index: 0,
|
||||
text: response.output_text,
|
||||
});
|
||||
this.event("response.content_part.done", {
|
||||
item_id: this.messageId,
|
||||
output_index: 0,
|
||||
output_index: outputIndex,
|
||||
content_index: 0,
|
||||
part,
|
||||
});
|
||||
this.event("response.output_item.done", { output_index: 0, item: outputMessage });
|
||||
this.event("response.output_item.done", { output_index: outputIndex, item: outputMessage });
|
||||
}
|
||||
this.event(terminalEvent, { response });
|
||||
}
|
||||
@@ -709,6 +857,7 @@ export async function registerResponseRoutes(
|
||||
effort: effectiveEffort,
|
||||
status: execution.validationErrors ? "incomplete" : "completed",
|
||||
finalText: execution.finalText,
|
||||
reasoningItems: execution.reasoningItems,
|
||||
...(execution.toolCalls ? { toolCalls: execution.toolCalls } : {}),
|
||||
...(execution.usage ? { usage: execution.usage } : {}),
|
||||
...(execution.validationErrors ? { incompleteReason: "structured_output_validation_failed" } : {}),
|
||||
@@ -718,6 +867,7 @@ export async function registerResponseRoutes(
|
||||
responseCompleted = true;
|
||||
|
||||
if (writer) {
|
||||
writer.addReasoningItems(execution.reasoningItems);
|
||||
if (validation) writer.addText(execution.chunks);
|
||||
writer.finish(
|
||||
terminalResponse,
|
||||
@@ -933,6 +1083,7 @@ function buildResponsesPrompt(
|
||||
"你只负责决定当前这一轮应该调用外部工具还是返回最终回答。外部工具由客户端执行,你不能模拟工具结果。",
|
||||
"不要使用 Command Code 自身的文件、终端、搜索、网络或其他工具替代外部 tools;只依据 input、tools 和 function_call_output 作出决定。",
|
||||
"严格执行 input 中的 system、developer、user 指令和当前 instructions,并结合 message、function_call、function_call_output 及其他已有 output item 理解完整历史。",
|
||||
"历史 reasoning item 只包含此前公开的 summary 文本记录,不是可恢复的内部推理状态;仅把 summary 当作普通历史上下文。",
|
||||
"只返回以下两个 JSON 对象之一,禁止 Markdown 代码围栏、前后说明和额外字段:",
|
||||
"最终回答:{\"type\":\"final\",\"content\":\"面向用户的完整回答\"}",
|
||||
"调用工具:{\"type\":\"tool_calls\",\"calls\":[{\"name\":\"工具名称\",\"arguments\":{}}]}",
|
||||
@@ -948,6 +1099,7 @@ function buildResponsesPrompt(
|
||||
return [
|
||||
"下面 JSON 对象是外部客户端提交的完整 Responses 文本任务和本地重建的响应链。",
|
||||
"按 input 中的 system、developer、user、assistant 角色与顺序理解上下文;当前 instructions 是本次请求的高优先级开发者指令。",
|
||||
"历史 reasoning item 只包含此前公开的 summary 文本记录,不是可恢复的内部推理状态;仅把 summary 当作普通历史上下文。",
|
||||
"严格执行最后一个用户任务。不要复述 JSON,不要输出角色标签,不要暴露内部思考。",
|
||||
formatDirective(request.text.format),
|
||||
"JSON 数据开始:",
|
||||
@@ -1006,6 +1158,7 @@ async function executeResponse(options: {
|
||||
return {
|
||||
finalText: first.result.finalText,
|
||||
chunks: first.finalChunks,
|
||||
reasoningItems: first.reasoningItems,
|
||||
...(first.result.usage ? { usage: first.result.usage } : {}),
|
||||
};
|
||||
}
|
||||
@@ -1015,6 +1168,7 @@ async function executeResponse(options: {
|
||||
return {
|
||||
finalText: first.result.finalText,
|
||||
chunks: first.finalChunks,
|
||||
reasoningItems: first.reasoningItems,
|
||||
...(first.result.usage ? { usage: first.result.usage } : {}),
|
||||
};
|
||||
}
|
||||
@@ -1032,6 +1186,7 @@ async function executeResponse(options: {
|
||||
return {
|
||||
finalText: second.result.finalText,
|
||||
chunks: second.finalChunks,
|
||||
reasoningItems: second.reasoningItems,
|
||||
...(combinedUsage ? { usage: combinedUsage } : {}),
|
||||
...(!secondValidation.valid ? { validationErrors: secondValidation.errors } : {}),
|
||||
};
|
||||
@@ -1094,6 +1249,7 @@ function executionFromToolDecision(
|
||||
return {
|
||||
finalText: "",
|
||||
chunks: [],
|
||||
reasoningItems: attempt.reasoningItems,
|
||||
toolCalls: decision.toolCalls,
|
||||
...(attempt.result.usage ? { usage: attempt.result.usage } : {}),
|
||||
};
|
||||
@@ -1101,6 +1257,7 @@ function executionFromToolDecision(
|
||||
return {
|
||||
finalText: decision.content,
|
||||
chunks: [decision.content],
|
||||
reasoningItems: attempt.reasoningItems,
|
||||
...(attempt.result.usage ? { usage: attempt.result.usage } : {}),
|
||||
};
|
||||
}
|
||||
@@ -1134,23 +1291,32 @@ async function executeAttempt(
|
||||
writer?: ResponsesSseWriter,
|
||||
): Promise<AttemptResult> {
|
||||
const accumulator = new FinalTurnAccumulator();
|
||||
const result = await runCommandCode(
|
||||
options.config,
|
||||
options.cliModel,
|
||||
options.effort,
|
||||
prompt,
|
||||
options.signal,
|
||||
{
|
||||
timeoutMs: options.deadline - Date.now(),
|
||||
onEvent: (event) => {
|
||||
const chunks = accumulator.event(event);
|
||||
if (chunks && writer) writer.addText(chunks);
|
||||
const reasoning = new ResponseReasoningAccumulator(writer);
|
||||
let result: CommandResult;
|
||||
try {
|
||||
result = await runCommandCode(
|
||||
options.config,
|
||||
options.cliModel,
|
||||
options.effort,
|
||||
prompt,
|
||||
options.signal,
|
||||
{
|
||||
timeoutMs: options.deadline - Date.now(),
|
||||
onEvent: (event) => {
|
||||
reasoning.event(event);
|
||||
const chunks = accumulator.event(event);
|
||||
if (chunks && writer) writer.addText(chunks);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
);
|
||||
} catch (error) {
|
||||
reasoning.finish();
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
result,
|
||||
finalChunks: accumulator.finalChunks ?? [result.finalText],
|
||||
reasoningItems: reasoning.finish(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1211,6 +1377,7 @@ function createResponse(options: {
|
||||
effort: string;
|
||||
status: ResponseStatus;
|
||||
finalText?: string;
|
||||
reasoningItems?: ResponseOutputReasoning[];
|
||||
toolCalls?: FunctionToolCall[];
|
||||
usage?: CommandUsage;
|
||||
error?: ResponseError;
|
||||
@@ -1219,7 +1386,7 @@ function createResponse(options: {
|
||||
const finalText = options.finalText ?? "";
|
||||
const terminal = options.status !== "in_progress";
|
||||
const outputStatus = options.status === "completed" ? "completed" : "incomplete";
|
||||
const output: ResponseOutputItem[] = options.toolCalls
|
||||
const generatedOutput: ResponseOutputItem[] = options.toolCalls
|
||||
? options.toolCalls.map((call) => ({
|
||||
id: newFunctionCallItemId(),
|
||||
type: "function_call",
|
||||
@@ -1237,6 +1404,10 @@ function createResponse(options: {
|
||||
content: [{ type: "output_text", text: finalText, annotations: [] }],
|
||||
}]
|
||||
: [];
|
||||
const output: ResponseOutputItem[] = [
|
||||
...(options.reasoningItems ?? []),
|
||||
...generatedOutput,
|
||||
];
|
||||
return {
|
||||
id: options.id,
|
||||
object: "response",
|
||||
@@ -1623,6 +1794,10 @@ function newFunctionCallOutputItemId(): string {
|
||||
return `fco_local_${randomHexId()}`;
|
||||
}
|
||||
|
||||
function newReasoningItemId(): string {
|
||||
return `rs_local_${randomHexId()}`;
|
||||
}
|
||||
|
||||
function randomHexId(): string {
|
||||
return randomUUID().replaceAll("-", "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user