openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
193 lines • 7.15 kB
JavaScript
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { ISSUE_UTILS } from '../devtools/issueDescriptions.js';
import { DevTools } from '../third_party/index.js';
import { logger } from '../utils/logger.js';
export class IssueFormatter {
#issue;
#options;
constructor(issue, options) {
this.#issue = issue;
this.#options = options;
}
toString() {
return convertIssueConciseToString(this.toJSON());
}
toStringDetailed() {
return convertIssueDetailedToString(this.toJSONDetailed());
}
toJSON() {
return {
type: 'issue',
title: this.#getTitle(),
count: this.#issue.getAggregatedIssuesCount(),
id: this.#options.id,
};
}
toJSONDetailed() {
return {
id: this.#options.id,
type: 'issue',
count: this.#issue.getAggregatedIssuesCount(),
title: this.#getTitle(),
description: this.#getDescription(),
links: this.#issue.getDescription()?.links,
affectedResources: this.#getAffectedResources(),
};
}
#getAffectedResources() {
const issues = this.#issue.getAllIssues();
const affectedResources = [];
for (const singleIssue of issues) {
const details = singleIssue.details();
if (!details) {
continue;
}
// We send the remaining details as untyped JSON because the DevTools
// frontend code is currently not re-usable.
const data = structuredClone(details);
let uid;
let request;
if ('violatingNodeId' in details &&
details.violatingNodeId &&
this.#options.elementIdResolver) {
uid = this.#options.elementIdResolver(details.violatingNodeId);
delete data.violatingNodeId;
}
if ('nodeId' in details &&
details.nodeId &&
this.#options.elementIdResolver) {
uid = this.#options.elementIdResolver(details.nodeId);
delete data.nodeId;
}
if ('documentNodeId' in details &&
details.documentNodeId &&
this.#options.elementIdResolver) {
uid = this.#options.elementIdResolver(details.documentNodeId);
delete data.documentNodeId;
}
if ('request' in details && details.request) {
request = details.request.url;
if (details.request.requestId && this.#options.requestIdResolver) {
const resolvedId = this.#options.requestIdResolver(details.request.requestId);
if (resolvedId) {
request = resolvedId;
const requestData = data.request;
delete requestData.requestId;
}
}
}
// These fields has no use for the MCP client (redundant or irrelevant).
delete data.errorType;
delete data.frameId;
affectedResources.push({
uid,
data: data,
request,
});
}
return affectedResources;
}
isValid() {
return this.#getTitle() !== undefined;
}
// Helper to extract title
#getTitle() {
const markdownDescription = this.#issue.getDescription();
const filename = markdownDescription?.file;
if (!filename) {
logger?.(`no description found for issue:` + this.#issue.code());
return undefined;
}
// We already have the description logic in #getDescription, but title extraction is separate
// We can reuse the logic or cache it.
// Ideally we should process markdown once.
const rawMarkdown = ISSUE_UTILS.getIssueDescription(filename);
if (!rawMarkdown) {
logger?.(`no markdown ${filename} found for issue:` + this.#issue.code());
return undefined;
}
try {
const processedMarkdown = DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription?.substitutions);
const markdownAst = DevTools.Marked.Marked.lexer(processedMarkdown);
const title = DevTools.MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst);
if (!title) {
logger?.('cannot read issue title from ' + filename);
return undefined;
}
return title;
}
catch {
logger?.('error parsing markdown for issue ' + this.#issue.code());
return undefined;
}
}
#getDescription() {
const markdownDescription = this.#issue.getDescription();
const filename = markdownDescription?.file;
if (!filename) {
return undefined;
}
const rawMarkdown = ISSUE_UTILS.getIssueDescription(filename);
if (!rawMarkdown) {
return undefined;
}
try {
return DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription?.substitutions);
}
catch {
return undefined;
}
}
}
function convertIssueConciseToString(issue) {
return `msgid=${issue.id} [issue] ${issue.title ?? 'Unknown Issue'} (count: ${issue.count})`;
}
function convertIssueDetailedToString(issue) {
const result = [];
result.push(`ID: ${issue.id}`);
const bodyParts = [];
const description = issue.description;
let processedMarkdown = description?.trim();
// Remove heading in order not to conflict with the whole console message response markdown
if (processedMarkdown?.startsWith('# ')) {
processedMarkdown = processedMarkdown.substring(2).trimStart();
}
if (processedMarkdown) {
bodyParts.push(processedMarkdown);
}
else {
bodyParts.push(issue.title ?? 'Unknown Issue');
}
const links = issue.links;
if (links && links.length > 0) {
bodyParts.push('Learn more:');
for (const link of links) {
bodyParts.push(`[${link.linkTitle}](${link.link})`);
}
}
const affectedResources = issue.affectedResources;
if (affectedResources.length) {
bodyParts.push('### Affected resources');
bodyParts.push(...affectedResources.map(item => {
const details = [];
if (item.uid) {
details.push(`uid=${item.uid}`);
}
if (item.request) {
details.push((typeof item.request === 'number' ? `reqid=` : 'url=') +
item.request);
}
if (item.data) {
details.push(`data=${JSON.stringify(item.data)}`);
}
return details.join(' ');
}));
}
result.push(`Message: issue> ${bodyParts.join('\n')}`);
return result.join('\n');
}
//# sourceMappingURL=IssueFormatter.js.map