实现更新帖子的功能

This commit is contained in:
wood chen 2025-01-24 00:14:34 +08:00
parent c4513b5abc
commit 95e541e1a7
6 changed files with 154 additions and 37 deletions

View File

@ -1,17 +1,30 @@
# Publish to Discourse # Publish to Discourse
## How to use ## 配置key
下载插件, 在设置页面添加"论坛地址", "API密钥(需要管理员创建)", "用户名". 下载插件, 在设置页面添加"论坛地址", "API密钥(需要管理员创建)", "用户名".
![image](https://github.com/user-attachments/assets/6c75ebb6-d028-4055-9616-2fb2931932ff) ![image](https://github.com/user-attachments/assets/6c75ebb6-d028-4055-9616-2fb2931932ff)
## 发布帖子
在文档页面的右上角, 展开菜单, 选择"发布到discourse", 选择类别即可. 在文档页面的右上角, 展开菜单, 选择"发布到discourse", 选择类别即可.
![动图](./pics/20250124-000738.gif)
![image](https://github.com/user-attachments/assets/99ba2b27-9c83-4dc5-9536-1b6b12dc4787) ![image](https://github.com/user-attachments/assets/99ba2b27-9c83-4dc5-9536-1b6b12dc4787)
![image](https://github.com/user-attachments/assets/a30b210f-5913-419d-b0d8-ea280c159e61) ![image](https://github.com/user-attachments/assets/a30b210f-5913-419d-b0d8-ea280c159e61)
在发布帖子成功后, 会在笔记属性里添加一个"discourse_post_id"属性, 用于更新帖子.
## 更新帖子
在文档页面的右上角, 展开菜单, 选择"发布到discourse", 点击更新即可.
> 更新的前提是帖子本身是通过obsidian发布的.
![动图](./pics/20250124-001000.gif)

19
package-lock.json generated
View File

@ -1,13 +1,16 @@
{ {
"name": "obsidian-sample-plugin", "name": "obsidian-publish-to-discourse",
"version": "1.0.3", "version": "1.0.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "obsidian-sample-plugin", "name": "obsidian-publish-to-discourse",
"version": "1.0.3", "version": "1.0.3",
"license": "MIT", "license": "MIT",
"dependencies": {
"yaml": "^2.7.0"
},
"devDependencies": { "devDependencies": {
"@types/node": "^16.11.6", "@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/eslint-plugin": "5.29.0",
@ -2364,6 +2367,18 @@
"license": "ISC", "license": "ISC",
"peer": true "peer": true
}, },
"node_modules/yaml": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -24,5 +24,8 @@
"obsidian": "latest", "obsidian": "latest",
"tslib": "2.4.0", "tslib": "2.4.0",
"typescript": "4.7.4" "typescript": "4.7.4"
},
"dependencies": {
"yaml": "^2.7.0"
} }
} }

BIN
pics/20250124-000738.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

BIN
pics/20250124-001000.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

View File

@ -1,9 +1,14 @@
import { App, Menu, MenuItem, Plugin, Modal, requestUrl, TFile } from 'obsidian'; import { App, Menu, MenuItem, Plugin, Modal, requestUrl, TFile } from 'obsidian';
import { DEFAULT_SETTINGS, DiscourseSyncSettings, DiscourseSyncSettingsTab } from './config'; import { DEFAULT_SETTINGS, DiscourseSyncSettings, DiscourseSyncSettingsTab } from './config';
import * as yaml from 'yaml';
export default class DiscourseSyncPlugin extends Plugin { export default class DiscourseSyncPlugin extends Plugin {
settings: DiscourseSyncSettings; settings: DiscourseSyncSettings;
activeFile: { name: string; content: string }; activeFile: {
name: string;
content: string;
postId?: number; // 添加帖子ID字段
};
async onload() { async onload() {
await this.loadSettings(); await this.loadSettings();
this.addSettingTab(new DiscourseSyncSettingsTab(this.app, this)); this.addSettingTab(new DiscourseSyncSettingsTab(this.app, this));
@ -113,6 +118,10 @@ export default class DiscourseSyncPlugin extends Plugin {
} }
let content = this.activeFile.content; let content = this.activeFile.content;
// 检查是否包含帖子ID的frontmatter
const frontMatter = this.getFrontMatter(content);
const postId = frontMatter?.discourse_post_id;
// 过滤掉笔记属性部分 // 过滤掉笔记属性部分
content = content.replace(/^---[\s\S]*?---\n/, ''); content = content.replace(/^---[\s\S]*?---\n/, '');
@ -125,7 +134,14 @@ export default class DiscourseSyncPlugin extends Plugin {
content = content.replace(obsRef, discoRef); content = content.replace(obsRef, discoRef);
}); });
const body = JSON.stringify({ const isUpdate = postId !== undefined;
const endpoint = isUpdate ? `${this.settings.baseUrl}/posts/${postId}` : url;
const method = isUpdate ? "PUT" : "POST";
const body = JSON.stringify(isUpdate ? {
raw: content,
edit_reason: "Updated from Obsidian"
} : {
title: this.activeFile.name, title: this.activeFile.name,
raw: content, raw: content,
category: this.settings.category category: this.settings.category
@ -133,28 +149,44 @@ export default class DiscourseSyncPlugin extends Plugin {
try { try {
const response = await requestUrl({ const response = await requestUrl({
url: url, url: endpoint,
method: "POST", method: method,
contentType: "application/json", contentType: "application/json",
body, body,
headers, headers,
throw: false // 设置为 false 以获取错误响应 throw: false
}); });
if (response.status === 200) { if (response.status === 200) {
if (!isUpdate) {
try {
// 获取新帖子的ID
const responseData = response.json;
if (responseData && responseData.id) {
await this.updateFrontMatter(responseData.id);
} else {
return {
message: "Error",
error: "发布成功但无法获取帖子ID"
};
}
} catch (error) {
return {
message: "Error",
error: "发布成功但无法保存帖子ID"
};
}
}
return { message: "Success" }; return { message: "Success" };
} else { } else {
// 尝试从响应中获取错误信息
try { try {
const errorResponse = response.json; const errorResponse = response.json;
// Discourse 通常会在 errors 数组中返回错误信息
if (errorResponse.errors && errorResponse.errors.length > 0) { if (errorResponse.errors && errorResponse.errors.length > 0) {
return { return {
message: "Error", message: "Error",
error: errorResponse.errors.join('\n') error: errorResponse.errors.join('\n')
}; };
} }
// 有些错误可能在 error 字段中
if (errorResponse.error) { if (errorResponse.error) {
return { return {
message: "Error", message: "Error",
@ -162,20 +194,63 @@ export default class DiscourseSyncPlugin extends Plugin {
}; };
} }
} catch (parseError) { } catch (parseError) {
// 如果无法解析错误响应
return { return {
message: "Error", message: "Error",
error: `发布失败 (${response.status})` error: `${isUpdate ? '更新' : '发布'}失败 (${response.status})`
}; };
} }
} }
} catch (error) { } catch (error) {
return { return {
message: "Error", message: "Error",
error: `发布失败: ${error.message || '未知错误'}` error: `${isUpdate ? '更新' : '发布'}失败: ${error.message || '未知错误'}`
}; };
} }
return { message: "Error", error: "发布失败,请重试" }; return { message: "Error", error: `${isUpdate ? '更新' : '发布'}失败,请重试` };
}
// 获取frontmatter数据
private getFrontMatter(content: string): any {
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (fmMatch) {
try {
return yaml.parse(fmMatch[1]);
} catch (e) {
return null;
}
}
return null;
}
// 更新frontmatter添加帖子ID
private async updateFrontMatter(postId: number) {
try {
// 获取当前活动文件
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) {
console.error('No active file found');
return;
}
const content = await this.app.vault.read(activeFile);
const fm = this.getFrontMatter(content);
let newContent: string;
if (fm) {
// 更新现有的frontmatter
const updatedFm = { ...fm, discourse_post_id: postId };
newContent = content.replace(/^---\n[\s\S]*?\n---\n/, `---\n${yaml.stringify(updatedFm)}---\n`);
} else {
// 添加新的frontmatter
const newFm = { discourse_post_id: postId };
newContent = `---\n${yaml.stringify(newFm)}---\n${content}`;
}
await this.app.vault.modify(activeFile, newContent);
console.log('Successfully updated frontmatter with post ID:', postId);
} catch (error) {
console.error('Error updating frontmatter:', error);
}
} }
private async fetchCategories() { private async fetchCategories() {
@ -217,9 +292,12 @@ export default class DiscourseSyncPlugin extends Plugin {
const syncDiscourse = (item: MenuItem) => { const syncDiscourse = (item: MenuItem) => {
item.setTitle("发布到 Discourse"); item.setTitle("发布到 Discourse");
item.onClick(async () => { item.onClick(async () => {
const content = await this.app.vault.read(file);
const fm = this.getFrontMatter(content);
this.activeFile = { this.activeFile = {
name: file.basename, name: file.basename,
content: await this.app.vault.read(file) content: content,
postId: fm?.discourse_post_id
}; };
await this.syncToDiscourse(); await this.syncToDiscourse();
}); });
@ -298,21 +376,23 @@ export class SelectCategoryModal extends Modal {
contentEl.empty(); contentEl.empty();
contentEl.addClass('discourse-sync-modal'); contentEl.addClass('discourse-sync-modal');
contentEl.createEl("h1", { text: '选择发布分类' }); const isUpdate = this.plugin.activeFile.postId !== undefined;
contentEl.createEl("h1", { text: isUpdate ? '更新帖子' : '发布到 Discourse' });
// 创建选择器容器 // 创建选择器容器
const selectContainer = contentEl.createEl('div', { cls: 'select-container' }); const selectContainer = contentEl.createEl('div', { cls: 'select-container' });
selectContainer.createEl('label', { text: '分类' }); if (!isUpdate) {
// 只在新建帖子时显示分类选择
const selectEl = selectContainer.createEl('select'); selectContainer.createEl('label', { text: '分类' });
const selectEl = selectContainer.createEl('select');
this.categories.forEach(category => { this.categories.forEach(category => {
const option = selectEl.createEl('option', { text: category.name }); const option = selectEl.createEl('option', { text: category.name });
option.value = category.id.toString(); option.value = category.id.toString();
}); });
}
const submitButton = contentEl.createEl('button', { const submitButton = contentEl.createEl('button', {
text: '发布', text: isUpdate ? '更新' : '发布',
cls: 'submit-button' cls: 'submit-button'
}); });
@ -320,13 +400,19 @@ export class SelectCategoryModal extends Modal {
const noticeContainer = contentEl.createEl('div'); const noticeContainer = contentEl.createEl('div');
submitButton.onclick = async () => { submitButton.onclick = async () => {
const selectedCategoryId = selectEl.value; if (!isUpdate) {
this.plugin.settings.category = parseInt(selectedCategoryId); const selectEl = contentEl.querySelector('select') as HTMLSelectElement;
await this.plugin.saveSettings(); if (!selectEl) {
return;
}
const selectedCategoryId = selectEl.value;
this.plugin.settings.category = parseInt(selectedCategoryId);
await this.plugin.saveSettings();
}
// 禁用提交按钮,显示加载状态 // 禁用提交按钮,显示加载状态
submitButton.disabled = true; submitButton.disabled = true;
submitButton.textContent = '发布中...'; submitButton.textContent = isUpdate ? '更新中...' : '发布中...';
try { try {
const reply = await this.plugin.postTopic(); const reply = await this.plugin.postTopic();
@ -336,7 +422,7 @@ export class SelectCategoryModal extends Modal {
if (reply.message === 'Success') { if (reply.message === 'Success') {
noticeContainer.createEl('div', { noticeContainer.createEl('div', {
cls: 'notice success', cls: 'notice success',
text: '✓ 发布成功!' text: isUpdate ? '✓ 更新成功!' : '✓ 发布成功!'
}); });
// 成功后延迟关闭 // 成功后延迟关闭
setTimeout(() => { setTimeout(() => {
@ -346,13 +432,13 @@ export class SelectCategoryModal extends Modal {
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' }); const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
errorContainer.createEl('div', { errorContainer.createEl('div', {
cls: 'error-title', cls: 'error-title',
text: '发布失败' text: isUpdate ? '更新失败' : '发布失败'
}); });
// 显示 Discourse 返回的具体错误信息 // 显示 Discourse 返回的具体错误信息
errorContainer.createEl('div', { errorContainer.createEl('div', {
cls: 'error-message', cls: 'error-message',
text: reply.error || '发布失败,请重试' text: reply.error || (isUpdate ? '更新失败,请重试' : '发布失败,请重试')
}); });
// 添加重试按钮 // 添加重试按钮
@ -363,7 +449,7 @@ export class SelectCategoryModal extends Modal {
retryButton.onclick = () => { retryButton.onclick = () => {
noticeContainer.empty(); noticeContainer.empty();
submitButton.disabled = false; submitButton.disabled = false;
submitButton.textContent = '发布'; submitButton.textContent = isUpdate ? '更新' : '发布';
}; };
} }
} catch (error) { } catch (error) {
@ -371,7 +457,7 @@ export class SelectCategoryModal extends Modal {
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' }); const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
errorContainer.createEl('div', { errorContainer.createEl('div', {
cls: 'error-title', cls: 'error-title',
text: '发布出错' text: isUpdate ? '更新出错' : '发布出错'
}); });
errorContainer.createEl('div', { errorContainer.createEl('div', {
cls: 'error-message', cls: 'error-message',
@ -386,14 +472,14 @@ export class SelectCategoryModal extends Modal {
retryButton.onclick = () => { retryButton.onclick = () => {
noticeContainer.empty(); noticeContainer.empty();
submitButton.disabled = false; submitButton.disabled = false;
submitButton.textContent = '发布'; submitButton.textContent = isUpdate ? '更新' : '发布';
}; };
} }
// 如果发生错误,重置按钮状态 // 如果发生错误,重置按钮状态
if (submitButton.disabled) { if (submitButton.disabled) {
submitButton.disabled = false; submitButton.disabled = false;
submitButton.textContent = '发布'; submitButton.textContent = isUpdate ? '更新' : '发布';
} }
}; };
} }