diff --git a/custom.d.ts b/custom.d.ts index 0b66d57b..8347f97b 100644 --- a/custom.d.ts +++ b/custom.d.ts @@ -23,4 +23,5 @@ * questions. */ -declare module "zhi-notion-markdown" \ No newline at end of file +declare module "zhi-notion-markdown" +declare module "uuid" \ No newline at end of file diff --git a/package.json b/package.json index 8788644c..b0e8b80a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "syncWidgetRepo": "python scripts/sync_widget_repo.py" }, "devDependencies": { + "@halo-dev/api-client": "^2.9.0", "@terwer/eslint-config-custom": "^1.3.6", "@types/crypto-js": "^4.1.2", "@types/node": "^18.17.17", @@ -60,7 +61,9 @@ "cross-fetch": "^3.1.8", "crypto-js": "^4.1.1", "element-plus": "^2.3.14", + "gray-matter": "^4.0.3", "js-base64": "^3.7.5", + "js-yaml": "^4.1.0", "lodash": "^4.17.21", "pinia": "^2.1.6", "shorthash2": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed887670..fca05b49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@element-plus/icons-vue': specifier: ^2.1.0 @@ -28,9 +32,15 @@ dependencies: element-plus: specifier: ^2.3.14 version: 2.3.14(vue@3.3.4) + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 js-base64: specifier: ^3.7.5 version: 3.7.5 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -96,6 +106,9 @@ dependencies: version: 0.5.12 devDependencies: + '@halo-dev/api-client': + specifier: ^2.9.0 + version: 2.9.0 '@terwer/eslint-config-custom': specifier: ^1.3.6 version: 1.3.6(@nuxt/eslint-config@0.1.1)(@typescript-eslint/eslint-plugin@5.62.0)(astro-eslint-parser@0.13.3)(eslint-config-prettier@8.10.0)(eslint-config-turbo@1.10.14)(eslint-plugin-prettier@4.2.1)(eslint-plugin-svelte@2.33.2)(eslint-plugin-vue@9.17.0)(eslint@8.49.0)(prettier-plugin-svelte@2.10.1)(prettier@2.8.8)(typescript@5.2.2) @@ -765,6 +778,10 @@ packages: vue: 3.3.4 dev: false + /@halo-dev/api-client@2.9.0: + resolution: {integrity: sha512-VVnqGruJ+6fpbgPSMIcsO52HvxXRCzbdkC1NXUsUrj+trJX9f++nU1qxsgMFd1cK+XlzdGs3XrNI0eBckT+zxQ==} + dev: true + /@humanwhocodes/config-array@0.11.11: resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} engines: {node: '>=10.10.0'} @@ -3954,6 +3971,13 @@ packages: engines: {node: '>=6'} dev: true + /extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + dependencies: + is-extendable: 0.1.1 + dev: false + /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: false @@ -4238,6 +4262,16 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + dev: false + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -4496,6 +4530,11 @@ packages: hasBin: true dev: true + /is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + dev: false + /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -4788,6 +4827,11 @@ packages: dependencies: json-buffer: 3.0.1 + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: false + /known-css-properties@0.28.0: resolution: {integrity: sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==} dev: true @@ -6167,6 +6211,14 @@ packages: resolution: {integrity: sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==} dev: true + /section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + dev: false + /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -6401,6 +6453,11 @@ packages: dependencies: ansi-regex: 6.0.1 + /strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + dev: false + /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -7603,7 +7660,3 @@ packages: /zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} dev: false - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/src/adaptors/api/base/github/commonGithubApiAdaptor.ts b/src/adaptors/api/base/github/commonGithubApiAdaptor.ts index a24a7e18..abd3caef 100644 --- a/src/adaptors/api/base/github/commonGithubApiAdaptor.ts +++ b/src/adaptors/api/base/github/commonGithubApiAdaptor.ts @@ -32,7 +32,7 @@ import { DateUtil, HtmlUtil, StrUtil, YamlUtil } from "zhi-common" import { toRaw } from "vue" import { Base64 } from "js-base64" import { CommonGitlabConfig } from "~/src/adaptors/api/base/gitlab/commonGitlabConfig.ts" -import IdUtil from "~/src/utils/idUtil.ts" +import sypIdUtil from "~/src/utils/sypIdUtil.ts"; /** * Github API 适配器 @@ -88,7 +88,7 @@ class CommonGithubApiAdaptor extends BaseBlogApi { // 路径处理 const savePath = post.cate_slugs?.[0] ?? cfg.blogid - const filename = post.mdFilename ?? "auto-" + IdUtil.newID() + const filename = post.mdFilename ?? "auto-" + sypIdUtil.newID() const docPath = `${savePath}/${filename}.md` this.logger.info("将要最终发送到以下目录 =>", docPath) diff --git a/src/adaptors/api/base/gitlab/commonGitlabApiAdaptor.ts b/src/adaptors/api/base/gitlab/commonGitlabApiAdaptor.ts index a47f9aee..ebb2a579 100644 --- a/src/adaptors/api/base/gitlab/commonGitlabApiAdaptor.ts +++ b/src/adaptors/api/base/gitlab/commonGitlabApiAdaptor.ts @@ -33,7 +33,7 @@ import { StrUtil, YamlUtil } from "zhi-common" import { toRaw } from "vue" import { Base64 } from "js-base64" import { isDev } from "~/src/utils/constants.ts" -import IdUtil from "~/src/utils/idUtil.ts" +import sypIdUtil from "~/src/utils/sypIdUtil.ts"; /** * Gitlab API 适配器 @@ -90,7 +90,7 @@ class CommonGitlabApiAdaptor extends BaseBlogApi { // 路径处理 const savePath = post.cate_slugs?.[0] ?? cfg.blogid - const filename = post.mdFilename ?? "auto-" + IdUtil.newID() + const filename = post.mdFilename ?? "auto-" + sypIdUtil.newID() const docPath = `${savePath}/${filename}.md` this.logger.info("将要最终发送到以下目录 =>", docPath) diff --git a/src/adaptors/api/halo/HaloApiAdaptor.ts b/src/adaptors/api/halo/HaloApiAdaptor.ts index c2ead510..81b03d9a 100644 --- a/src/adaptors/api/halo/HaloApiAdaptor.ts +++ b/src/adaptors/api/halo/HaloApiAdaptor.ts @@ -26,9 +26,22 @@ import { HaloConfig } from "~/src/adaptors/api/halo/HaloConfig.ts" import { BaseBlogApi } from "~/src/adaptors/api/base/baseBlogApi.ts" import { createAppLogger } from "~/src/utils/appLogger.ts" -import { Post, UserBlog } from "zhi-blog-api" -import { ObjectUtil } from "zhi-common" +import { CategoryInfo, Post, UserBlog } from "zhi-blog-api" +import { AliasTranslator, JsonUtil, ObjectUtil, StrUtil } from "zhi-common" import { Base64 } from "js-base64" +import sypIdUtil from "~/src/utils/sypIdUtil.ts" +import { + Category, + CategoryList, + ListedPost, + ListedPostList, + Policy, + PostRequest, + Tag, + TagList, +} from "@halo-dev/api-client" +import { HaloPostMeta } from "~/src/adaptors/api/halo/HaloPostMeta.ts" +import HaloUtils from "~/src/adaptors/api/halo/haloUtils.ts" /** * Halo API 适配器 @@ -59,19 +72,305 @@ class HaloApiAdaptor extends BaseBlogApi { } public async newPost(post: Post, publish?: boolean): Promise { - const url = "" + const params: PostRequest = { + post: { + spec: { + title: "", + slug: "", + template: "", + cover: "", + deleted: false, + publish: false, + publishTime: undefined, + pinned: false, + allowComment: true, + visible: "PUBLIC", + priority: 0, + excerpt: { + autoGenerate: true, + raw: "", + }, + categories: [], + tags: [], + htmlMetas: [], + }, + apiVersion: "content.halo.run/v1alpha1", + kind: "Post", + metadata: { + name: "", + annotations: {}, + }, + }, + content: { + raw: "", + content: "", + rawType: "HTML", + }, + } + + params.content.raw = post.html + params.content.content = post.description + + if (StrUtil.isEmptyString(post.shortDesc)) { + params.post.spec.excerpt.autoGenerate = true + } else { + params.post.spec.excerpt.autoGenerate = false + params.post.spec.excerpt.raw = post.shortDesc + } + + params.post.metadata.name = sypIdUtil.randomUuid() + params.post.spec.title = post.title + params.post.spec.slug = post.wp_slug + + // 标签和分类 + if (post.categories && post.categories.length > 0) { + const categoryNames = await this.getCategoryNames(post.categories) + params.post.spec.categories = categoryNames + } + if (!StrUtil.isEmptyString(post.mt_keywords)) { + const tags = post.mt_keywords.split(",") + const tagNames = await this.getTagNames(tags) + params.post.spec.tags = tagNames + } + + // 草稿 + const res = await this.haloRequest("/apis/api.console.halo.run/v1alpha1/posts", params, "POST") + this.logger.debug("halo newPost res =>", res) + if (!res?.metadata?.name) { + throw new Error("Halo 文章发布失败") + } + this.logger.debug("halo 文章草稿完成") + + // 发布 + await this.haloRequest(`/apis/api.console.halo.run/v1alpha1/posts/${params.post.metadata.name}/publish`, {}, "PUT") + this.logger.debug("halo 文章发布完成") + + // 生成文章ID + const postidMeta = new HaloPostMeta(res.spec.slug, res.metadata.name, post.dateCreated) + this.logger.debug("postidMeta =>", postidMeta) + return JSON.stringify(postidMeta) + } + + public async editPost(postid: string, post: Post, publish?: boolean): Promise { + let flag = false + try { + const haloPostKey = this.getHaloPostidKey(postid) + const name = haloPostKey.name + const params: PostRequest = { + post: { + spec: { + title: "", + slug: "", + template: "", + cover: "", + deleted: false, + publish: false, + publishTime: undefined, + pinned: false, + allowComment: true, + visible: "PUBLIC", + priority: 0, + excerpt: { + autoGenerate: true, + raw: "", + }, + categories: [], + tags: [], + htmlMetas: [], + }, + apiVersion: "content.halo.run/v1alpha1", + kind: "Post", + metadata: { + name: "", + annotations: {}, + }, + }, + content: { + raw: "", + content: "", + rawType: "HTML", + }, + } - return "" + params.content.raw = post.html + params.content.content = post.description + + if (StrUtil.isEmptyString(post.shortDesc)) { + params.post.spec.excerpt.autoGenerate = true + } else { + params.post.spec.excerpt.autoGenerate = false + params.post.spec.excerpt.raw = post.shortDesc + } + + params.post.metadata.name = name + params.post.spec.title = post.title + params.post.spec.slug = post.wp_slug + + // 标签和分类 + if (post.categories && post.categories.length > 0) { + const categoryNames = await this.getCategoryNames(post.categories) + params.post.spec.categories = categoryNames + } + if (!StrUtil.isEmptyString(post.mt_keywords)) { + const tags = post.mt_keywords.split(",") + const tagNames = await this.getTagNames(tags) + params.post.spec.tags = tagNames + } + + await this.haloRequest(`/apis/content.halo.run/v1alpha1/posts/${name}`, params.post, "PUT") + await this.haloRequest(`/apis/api.console.halo.run/v1alpha1/posts/${name}/content`, params.content, "PUT") + } catch (e) { + this.logger.error("Halo文章更新失败", e) + } + + return flag + } + + public async getPreviewUrl(postid: string): Promise { + const haloPostKey = this.getHaloPostidKey(postid) + const postUrl = this.cfg.previewUrl + .replace("{slug}", haloPostKey.slug) + .replace("{name}", haloPostKey.name) + .replace("{year}", haloPostKey.year) + .replace("{month}", haloPostKey.month) + .replace("{day}", haloPostKey.day) + return StrUtil.pathJoin(this.cfg.home ?? "", postUrl) + } + + public async deletePost(postid: string): Promise { + const haloPostKey = this.getHaloPostidKey(postid) + // const unPublishUrl = `/apis/api.console.halo.run/v1alpha1/posts/${haloPostKey.name}/unpublish` + const recycleUrl = `/apis/api.console.halo.run/v1alpha1/posts/${haloPostKey.name}/recycle` + const res = await this.haloRequest(recycleUrl, {}, "PUT") + this.logger.debug("halo deletePost res =>", res) + + if (!res?.metadata?.name) { + throw new Error("Halo 文章删除失败") + } + return true + } + + public async getCategories(): Promise { + const cats = [] as CategoryInfo[] + + const hcs: any[] = await this.getHaloCategories() + if (hcs && hcs.length > 0) { + // 数据适配 + hcs.forEach((item: any) => { + const cat = new CategoryInfo() + cat.categoryId = item.spec.slug + cat.categoryName = item.spec.displayName + cat.description = item.spec.displayName + cat.categoryDescription = item.spec.description + cats.push(cat) + }) + this.logger.debug("get halo categories =>", cats) + } + + return cats } // ================ // private methods // ================ + /** + * 获取封装的postid + * + * @param postid + * @private postid + */ + private getHaloPostidKey(postid: string): HaloPostMeta { + const postidJson = JsonUtil.safeParse(postid, {} as HaloPostMeta) + return postidJson + } + + public async getCategoryNames(displayNames: string[]): Promise { + const allCategories = await this.getHaloCategories() + + const notExistDisplayNames = displayNames.filter( + (name) => !allCategories.find((item) => item.spec.displayName === name) + ) + + const promises = notExistDisplayNames.map(async (name, index) => { + const slug = await AliasTranslator.getPageSlug(name, true) + return this.haloRequest( + "/apis/content.halo.run/v1alpha1/categories", + { + spec: { + displayName: name, + slug: slug, + description: "", + cover: "", + template: "", + priority: allCategories.length + index, + children: [], + }, + apiVersion: "content.halo.run/v1alpha1", + kind: "Category", + metadata: { name: "", generateName: "category-" }, + }, + "POST" + ) + }) + + const newCategories = await Promise.all(promises) + + const existNames = displayNames + .map((name) => { + const found = allCategories.find((item) => item.spec.displayName === name) + return found ? found.metadata.name : undefined + }) + .filter(Boolean) as string[] + + return [...existNames, ...newCategories.map((item) => item.data.metadata.name)] + } + private async getHaloCategories() { const categories = await this.haloRequest("/apis/content.halo.run/v1alpha1/categories", {}, "GET") return Promise.resolve(categories.items) } + public async getTagNames(displayNames: string[]): Promise { + const allTags = await this.getHaloTags() + + const notExistDisplayNames = displayNames.filter((name) => !allTags.find((item) => item.spec.displayName === name)) + + const promises = notExistDisplayNames.map(async (name) => { + const slug = await AliasTranslator.getPageSlug(name, true) + return this.haloRequest( + "/apis/content.halo.run/v1alpha1/tags", + { + spec: { + displayName: name, + slug: slug, + color: "#ffffff", + cover: "", + }, + apiVersion: "content.halo.run/v1alpha1", + kind: "Tag", + metadata: { name: "", generateName: "tag-" }, + }, + "POST" + ) + }) + + const newTags = await Promise.all(promises) + + const existNames = displayNames + .map((name) => { + const found = allTags.find((item) => item.spec.displayName === name) + return found ? found.metadata.name : undefined + }) + .filter(Boolean) as string[] + + return [...existNames, ...newTags.map((item) => item.data.metadata.name)] + } + + private async getHaloTags() { + const categories = await this.haloRequest("/apis/content.halo.run/v1alpha1/tags", {}, "GET") + return Promise.resolve(categories.items) + } + /** * 向 Halo 请求数据 * diff --git a/src/adaptors/api/halo/HaloConfig.ts b/src/adaptors/api/halo/HaloConfig.ts index db890179..6a332c6d 100644 --- a/src/adaptors/api/halo/HaloConfig.ts +++ b/src/adaptors/api/halo/HaloConfig.ts @@ -42,8 +42,8 @@ class HaloConfig extends CommonBlogConfig { this.home = "[your-halo-home]" this.apiUrl = "[your-halo-api-url]" - this.previewUrl = "/?p=[postid]" - this.pageType = PageTypeEnum.Markdown + this.previewUrl = "/archives/{slug}" + this.pageType = PageTypeEnum.Html this.usernameEnabled = true this.showTokenTip = false this.allowPreviewUrlChange = true diff --git a/src/adaptors/api/halo/HaloPostMeta.ts b/src/adaptors/api/halo/HaloPostMeta.ts new file mode 100644 index 00000000..d364e2f8 --- /dev/null +++ b/src/adaptors/api/halo/HaloPostMeta.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023, Terwer . All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Terwer designates this + * particular file as subject to the "Classpath" exception as provided + * by Terwer in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com + * or visit www.terwer.space if you need additional information or have any + * questions. + */ + +import { DateUtil } from "zhi-common" + +/** + * Halo 平台的一些元数据 + * + * @author terwer + * @version 1.15.0 + * @since 1.15.0 + */ +class HaloPostMeta { + public slug: string + public name: string + public year: string + public month: string + public day: string + + constructor(slug: string, name: string, dateCreated: Date) { + this.slug = slug + this.name = name + + const created = DateUtil.formatIsoToZhDate(dateCreated.toISOString(), true) + const datearr = created.split(" ")[0] + const numarr = datearr.split("-") + this.year = numarr[0] + this.month = numarr[1] + this.day = numarr[2] + } +} + +export { HaloPostMeta } diff --git a/src/adaptors/api/halo/haloUtils.ts b/src/adaptors/api/halo/haloUtils.ts new file mode 100644 index 00000000..8a420eca --- /dev/null +++ b/src/adaptors/api/halo/haloUtils.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, Terwer . All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Terwer designates this + * particular file as subject to the "Classpath" exception as provided + * by Terwer in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com + * or visit www.terwer.space if you need additional information or have any + * questions. + */ + +import * as yaml from "js-yaml" +import * as matter from "gray-matter" + +/** + * Halo 平台工具类 + * + * @author terwer + * @version 1.15.0 + * @since 1.15.0 + */ +class HaloUtils { + private static options = { + engines: { + yaml: { + parse: (input: string) => yaml.load(input) as object, + stringify: (data: object) => { + return yaml.dump(data, { + styles: { "!!null": "empty" }, + }) + }, + }, + }, + } + + public static readMatter(content: string) { + return matter(content, this.options) + } + + public static mergeMatter(content: string, data: object) { + return matter.stringify(content, data, this.options) + } +} + +export default HaloUtils diff --git a/src/adaptors/api/halo/useHaloApi.ts b/src/adaptors/api/halo/useHaloApi.ts index c8ff1d71..a83d424d 100644 --- a/src/adaptors/api/halo/useHaloApi.ts +++ b/src/adaptors/api/halo/useHaloApi.ts @@ -76,7 +76,7 @@ const useHaloApi = async (key: string, newCfg?: HaloConfig) => { cfg.usernameEnabled = true // 标签 - cfg.tagEnabled = false + cfg.tagEnabled = true // Halo 使用多选分类 cfg.cateEnabled = true cfg.categoryType = CategoryTypeEnum.CategoryType_Multi diff --git a/src/components/set/publish/singleplatform/commonblog/HaloSetting.vue b/src/components/set/publish/singleplatform/commonblog/HaloSetting.vue index c90a0a7e..24decef3 100644 --- a/src/components/set/publish/singleplatform/commonblog/HaloSetting.vue +++ b/src/components/set/publish/singleplatform/commonblog/HaloSetting.vue @@ -42,11 +42,11 @@ const { t } = useVueI18n() const { cfg } = await useHaloApi(props.apiType) const haloCfg = cfg as HaloConfig const haloPlaceholder = new HaloPlaceholder() -haloPlaceholder.homePlaceholder = t("setting.yuque.home.tip") -haloPlaceholder.usernamePlaceholder = t("setting.yuque.username.tip") -haloPlaceholder.passwordPlaceholder = t("setting.yuque.password.tip") -haloPlaceholder.apiUrlPlaceholder = t("setting.yuque.apiurl.tip") -haloPlaceholder.previewUrlPlaceholder = t("setting.yuque.previewUrl.tip") +haloPlaceholder.homePlaceholder = t("setting.halo.home.tip") +haloPlaceholder.usernamePlaceholder = t("setting.halo.username.tip") +haloPlaceholder.passwordPlaceholder = t("setting.halo.password.tip") +haloPlaceholder.apiUrlPlaceholder = t("setting.halo.apiUrl.tip") +haloPlaceholder.previewUrlPlaceholder = t("setting.halo.previewUrl.tip") haloCfg.placeholder = haloPlaceholder // 处理事件的方法 diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 5ee5523c..36b189cf 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -449,6 +449,12 @@ export default { "setting.juejin.apiUrl.tip": "Usually fixed, do not modify", "setting.juejin.previewUrl.tip": "Juejin platform article preview rules", + "setting.halo.home.tip": "Halo homepage address, should include the domain and port, for example: http://localhost:8090", + "setting.halo.username.tip": "Halo blog login name", + "setting.halo.password.tip": "Halo blog login password", + "setting.halo.apiUrl.tip": "Halo blog API address, typically the same as the Halo homepage", + "setting.halo.previewUrl.tip": "Halo blog article preview rule, default /archives/{slug}, with placeholders {slug} {name} {year} {month} {day}, can be set in [halo-home-url]/console/settings?tab=routeRules", + "setting.picgo.refer.to": "For details, please refer to:", "setting.picgo.refer.to.online.doc": "Picgo configuration online documentation", "setting.picgo.picbed": "Picbed setting", diff --git a/src/locales/zh_CN.ts b/src/locales/zh_CN.ts index 100b34e0..9df5f10f 100644 --- a/src/locales/zh_CN.ts +++ b/src/locales/zh_CN.ts @@ -443,6 +443,13 @@ export default { "setting.juejin.apiUrl.tip": "通常固定,请勿修改", "setting.juejin.previewUrl.tip": "掘金平台文章预览规则", + "setting.halo.home.tip": "Halo首页地址,需要包括域名端口,例如:http://localhost:8090", + "setting.halo.username.tip": "Halo博客登录名", + "setting.halo.password.tip": "Halo博客登录密码", + "setting.halo.apiUrl.tip": "Halo博客API地址,一般与Halo首页相同", + "setting.halo.previewUrl.tip": + "Halo博客文章预览规则,默认 /archives/{slug},占位符有 {slug} {name} {year} {month} {day},可在 [halo-home-url]/console/settings?tab=routeRules设置", + "setting.picgo.refer.to": "详情请参考:", "setting.picgo.refer.to.online.doc": "PicGO配置在线文档", "setting.picgo.picbed": "图床设置", @@ -588,7 +595,8 @@ export default { "config.ai.use.siyuan": "思源笔记内部,直接使用思源笔记配置,无需配置", "setting.blog.yamlLinkEnabled": "YAML永久链接", "distri.type.overide.warn": "注意:覆盖模式下会用当前数据覆盖所有选择的平台,请谨慎操作。", - "distri.type.merge.warn": "注意:为了保留平台数据,合并模式下,标题和摘要的修改无效,您可在在常规发布单独修改对应平台的标题和摘要,标签和分类会与所选择的平台数据合并。", + "distri.type.merge.warn": + "注意:为了保留平台数据,合并模式下,标题和摘要的修改无效,您可在在常规发布单独修改对应平台的标题和摘要,标签和分类会与所选择的平台数据合并。", "preference.setting.keepTitle": "不更新原始标题", "setting.blog.gitlab.url.tip": "Gitlab首页,例如:http://localhost:8002", @@ -597,5 +605,5 @@ export default { "setting.blog.gitlab.apiurl.tip": "Gitlab的API地址,一般与首页相同", "setting.blog.gitlab.previewUrl.tip": "文章预览地址,一般默认即可", - "main.force.cancel": "强制删除" + "main.force.cancel": "强制删除", } diff --git a/src/platforms/dynamicConfig.ts b/src/platforms/dynamicConfig.ts index fae4cea8..062e02ac 100644 --- a/src/platforms/dynamicConfig.ts +++ b/src/platforms/dynamicConfig.ts @@ -23,7 +23,7 @@ * questions. */ -import idUtil from "~/src/utils/idUtil.ts" +import sypIdUtil from "~/src/utils/sypIdUtil.ts" import { StrUtil } from "zhi-common" export class DynamicConfig { @@ -370,7 +370,7 @@ export function getSubPlatformTypeByKey(key: string): SubPlatformType { */ export function getNewPlatformKey(ptype: PlatformType, subtype: SubPlatformType): string { let ret: any - const newId = idUtil.newID() + const newId = sypIdUtil.newID() ret = ptype.toLowerCase() if (!StrUtil.isEmptyString(subtype) && SubPlatformType.NONE !== subtype) { diff --git a/src/utils/idUtil.ts b/src/utils/sypIdUtil.ts similarity index 81% rename from src/utils/idUtil.ts rename to src/utils/sypIdUtil.ts index f9be5f91..da7eddde 100644 --- a/src/utils/idUtil.ts +++ b/src/utils/sypIdUtil.ts @@ -41,9 +41,22 @@ const newUuid = () => { return uuidv4() } -const idUtil = { +/** + * 生成随机ID + */ +const randomUuid = (): string => { + const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0, + v = c === "x" ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) + return uuid +} + +const sypIdUtil = { newUuid, newID, + randomUuid, } -export default idUtil +export default sypIdUtil diff --git a/testdata/halo/halo.http b/testdata/halo/halo.http new file mode 100644 index 00000000..84290856 --- /dev/null +++ b/testdata/halo/halo.http @@ -0,0 +1,169 @@ +### 更新 +GET http://localhost:8090/apis/content.halo.run/v1alpha1/posts/43e02387-66d4-4c81-b682-d9912f8ae1f0 +Accept: application/json, text/plain, */* +Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 +Cache-Control: no-cache +Connection: keep-alive +Cookie: {{cookie}} +Pragma: no-cache +Referer: http://localhost:8090/console/posts/editor?name=43e02387-66d4-4c81-b682-d9912f8ae1f0 +Sec-Fetch-Dest: empty +Sec-Fetch-Mode: cors +Sec-Fetch-Site: same-origin +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 +X-Requested-With: XMLHttpRequest +X-XSRF-TOKEN: b3cf960c-9944-4862-8ac5-900d094b5466 +sec-ch-ua: "Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117" +sec-ch-ua-mobile: ?0 +sec-ch-ua-platform: "macOS" + +### 更新2 +PUT http://localhost:8090/apis/content.halo.run/v1alpha1/posts/43e02387-66d4-4c81-b682-d9912f8ae1f0 +Accept: application/json, text/plain, */* +Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 +Cache-Control: no-cache +Connection: keep-alive +Cookie: Webstorm-f47ab5ea=35e776f8-03d5-4354-971d-34b55da326f0; _ga=GA1.1.659711968.1668949168; __gsas=ID=f27169070a6b3d21:T=1673082680:S=ALNI_MZxBVm1QTHQ-A4-u8bbUtkZw5r3Lg; sidebar_collapsed=false; hide_no_ssh_message=false; hide_auto_devops_implicitly_enabled_banner_2=false; visitor_id=edba51f3-38d9-4d43-b237-f08608cd9a4f; Webstorm-f47ab5eb=07a16c94-2964-485d-82db-454f2a7c83bc; Idea-731bc924=f3abf4bc-94d6-4c2e-bace-ebd6be9289f4; Hm_lvt_503f098e7e5b3a5b5d8c5fc2938af002=1677301457; csrftoken=66uGnsvvhDRQjzxDQElN6rRid3E8oYJemIgUCaIsX6uzkk2aq9nA7t9ipPan95BT; _clck=kj8afs|1|f9k|0; _ga_L7WEXVQCR9=GS1.1.1679464226.7.0.1679464226.0.0.0; cookieconsent_status=dismiss; Webstorm-f47ab9aa=9ac0fe04-029a-4068-a681-b5e1f5753382; wp-settings-1=posts_list_mode%3Dlist%26editor%3Dtinymce; wp-settings-time-1=1685353721; Idea-731bcce3=51c78b23-6602-44bd-8f61-b8d920d8fe07; app-config=%7B%7D; event_filter=all; super_sidebar_collapsed=false; username-localhost-8888=2|1:0|10:1694855542|23:username-localhost-8888|196:eyJ1c2VybmFtZSI6ICIwZmI0YTJmMWNlNzA0MTllOTQ4N2YzZjZjYmFjZDE1YyIsICJuYW1lIjogIkFub255bW91cyBFcmlub21lIiwgImRpc3BsYXlfbmFtZSI6ICJBbm9ueW1vdXMgRXJpbm9tZSIsICJpbml0aWFscyI6ICJBRSIsICJjb2xvciI6IG51bGx9|b7564a95f2cb5c2aa9010ad5a3e29a1afa8265f660890a5615bba2b4331fd286; Hm_lvt_ecf13ffee9f4083d9a74b337b541f90b=1694445876,1694596707,1694700380,1694867559; known_sign_in=QlNLMzc2dmg2ZnQzbCtSSlVYUUZpZy84YW1VQUxIRnVxRkt1REJ4aXlHMEY1NmFzMmsya0w4YUEySGgrcjZ2SDVPVTZXL01ZUU5qSFpBQTNjMXVxaVJGTE9oZk90UDM4b0tXR1d2cVV3RlJuZDU4QnBYUGNvbmRueXdKZ05kNDUtLWlxbmdyU1o3QkVJSTdaWEMxQTgxSXc9PQ%3D%3D--fa89e9553241e5ead8890a0a1ee5ae92059de6ce; XSRF-TOKEN=b3cf960c-9944-4862-8ac5-900d094b5466; SESSION=0661b43b-8336-412a-9435-d5b50ed16895 +Origin: http://localhost:8090 +Pragma: no-cache +Referer: http://localhost:8090/console/posts/editor?name=43e02387-66d4-4c81-b682-d9912f8ae1f0 +Sec-Fetch-Dest: empty +Sec-Fetch-Mode: cors +Sec-Fetch-Site: same-origin +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 +X-Requested-With: XMLHttpRequest +X-XSRF-TOKEN: b3cf960c-9944-4862-8ac5-900d094b5466 +sec-ch-ua: "Google Chrome";v="117", "Not;A=Brand";v="8", "Chromium";v="117" +sec-ch-ua-mobile: ?0 +sec-ch-ua-platform: "macOS" +Content-Type: application/json + +{ + "spec": { + "title": "jdk8编译", + "slug": "jdk8-2ggilv", + "releaseSnapshot": "55017e8b-80fe-4efa-8622-3d109692cc5d", + "headSnapshot": "55017e8b-80fe-4efa-8622-3d109692cc5d", + "baseSnapshot": "55017e8b-80fe-4efa-8622-3d109692cc5d", + "owner": "terwer", + "template": "", + "cover": "", + "deleted": false, + "publish": true, + "publishTime": "2023-09-20T14:10:45.272080040Z", + "pinned": false, + "allowComment": true, + "visible": "PUBLIC", + "priority": 0, + "excerpt": { + "autoGenerate": false, + "raw": "本文介绍了在 macOS 系统上编译 OpenJDK 的准备步骤,包括安装必要的依赖和解决常见问题。涵盖了安装 Xcode、配置 FreeType、使用 ccache 加速编译、解决 debug symbols 和 objcopy 相关问题,以及编译 OpenJDK 的步骤。适合需要在 macOS 上编译 OpenJDK 的开发者参考。" + }, + "categories": [ + "开发工具" + ], + "tags": [ + "编译", + "OpenJDK", + "macOS", + "配置", + "调试", + "tag-QfWEK", + "tag-TnfxE", + "tag-hXfGP" + ], + "htmlMetas": [] + }, + "status": { + "phase": "PUBLISHED", + "conditions": [ + { + "type": "PUBLISHED", + "status": "TRUE", + "lastTransitionTime": "2023-09-20T14:10:45.272067209Z", + "message": "Post published successfully.", + "reason": "Published" + }, + { + "type": "DRAFT", + "status": "TRUE", + "lastTransitionTime": "2023-09-20T14:10:45.150131356Z", + "message": "Drafted post successfully.", + "reason": "DraftedSuccessfully" + } + ], + "permalink": "/archives/jdk8-2ggilv", + "excerpt": "本文介绍了在 macOS 系统上编译 OpenJDK 的准备步骤,包括安装必要的依赖和解决常见问题。涵盖了安装 Xcode、配置 FreeType、使用 ccache 加速编译、解决 debug symbols 和 objcopy 相关问题,以及编译 OpenJDK 的步骤。适合需要在 macOS 上编译 OpenJDK 的开发者参考。", + "inProgress": false, + "contributors": [ + "terwer" + ], + "lastModifyTime": "2023-09-20T14:10:45.144073163Z" + }, + "apiVersion": "content.halo.run/v1alpha1", + "kind": "Post", + "metadata": { + "finalizers": [ + "post-protection" + ], + "name": "43e02387-66d4-4c81-b682-d9912f8ae1f0", + "labels": { + "content.halo.run/deleted": "false", + "content.halo.run/owner": "terwer", + "content.halo.run/visible": "PUBLIC", + "content.halo.run/published": "true", + "content.halo.run/archive-year": "2023", + "content.halo.run/archive-month": "09", + "content.halo.run/archive-day": "20" + }, + "annotations": { + "checksum/config": "4376ce510afb56f97718a07cd4343f2a291430b73d058ac1b3db2777f30c0781", + "content.halo.run/permalink-pattern": "/archives/{slug}", + "content.halo.run/last-released-snapshot": "55017e8b-80fe-4efa-8622-3d109692cc5d", + "content.halo.run/preferred-editor": "default" + }, + "version": 16, + "creationTimestamp": "2023-09-20T14:10:45.137981382Z" + } +} + +### + + + + +### 删除文章 +PUT http://localhost:8090/apis/api.console.halo.run/v1alpha1/posts/d1148a2f-45a3-4222-9cc6-5a6a0d61708f/recycle +Cookie: {{cookie}} + +### 新建文章草稿 +PUT http://localhost:8090/apis/api.console.halo.run/v1alpha1/posts/3eb2030c-7614-4476-a40a-1a9438a0f9ad/content +Cookie: {{cookie}} +Content-Type: application/json + +{ + "raw": "

test

测试2

", + "content": "

test

测试3

", + "rawType": "HTML", + "snapshotName": "03fb3b5d-8b5d-4f89-b72f-676a67bc818e" +} + +### 新建标签 +POST http://localhost:8090/apis/content.halo.run/v1alpha1/tags +Cookie: {{cookie}} +Content-Type: application/json + +{ + "spec": { + "displayName": "测试3", + "slug": "ce-shi-3", + "color": "#ffffff", + "cover": "" + }, + "apiVersion": "content.halo.run/v1alpha1", + "kind": "Tag", + "metadata": { + "name": "", + "generateName": "tag-" + } +} \ No newline at end of file diff --git a/testdata/wework/wework.http b/testdata/wework/wework.http new file mode 100644 index 00000000..4dba95e1 --- /dev/null +++ b/testdata/wework/wework.http @@ -0,0 +1,69 @@ +# curl 'https://work.weixin.qq.com/wework_admin/message/sendmsg?lang=zh_CN&f=json&ajax=1&timeZoneInfo%5Bzone_offset%5D=-8&random=0.9698314368566994' +# -H 'authority: work.weixin.qq.com' +# -H 'accept: application/json, text/javascript, */*; q=0.01' +# -H 'accept-language: zh-CN,zh;q=0.9,en;q=0.8' +# -H 'cache-control: no-cache' +# -H 'content-type: application/x-www-form-urlencoded' +# -H 'cookie: pac_uid=0_76b5f59646cdc; tvfe_boss_uuid=dd726aa255a50875; RK=VJPt5BVxEo; ptcz=65c2c6b7f451559ff849effaa6075b80e9e0f462c291ef1c54b2ed30312b2bd3; pgv_pvid=4959488044; iip=0; _tc_unionid=bf3de621-2192-4aab-a46b-d48cf897c142; qq_domain_video_guid_verify=a7bf9669df468076; wwrtx.ref=direct; wwrtx.c_gdpr=0; wwrtx.refid=41317284992931586; wwrtx.ltype=1; wwrtx.vid=1688858336390504; wxpay.corpid=1970326256986275; wxpay.vid=1688858336390504; wwrtx.cs_ind=; wwrtx.logined=true; wwrtx.i18n_lan=zh; wwapidoc.sid=6EB866FA94861808FDAB4B1F46C8E0CCE76D97166EE82CC762A4376982F6C64A108E55B4AB6FBDD1C1DF92C86FBBA7F5D85D44E7D3E8BCD3B3EFFF34F2CB53E7; wwapidoc.token_wt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6IkNtc0tCZ2dDRUFFWUFoSWZuek9kQjlVS1dENFhENWM1Z0xHTXBkQ2drY0pSZmVPSUlCT0drc0gyMmhwQXZVWkc2RFlxQlNiOTVBcERPVzRtSnoxR29meHRiY1JjcTJrbWtUQ2hJVGJZZmR3eTJ4Sytha1FyWW1FcEtGUjJGdVZRUTNUOFR2MHdZMjZoNEI0VlpTSWlDaUFJeTR2WDlRRVEzTWNDR0lDQWdCQWlEM2QzYlc1bmMyVnpjMmx2Ym5OMmNnPT0iLCJpYXQiOjE2OTM3MzQwMDAsImV4cCI6MTY5MzczNTgwMH0.dca-YqJ5MZxhz3qCbak4q_6U-_XA-9SQ_FNYn4OMkVo; wwrtx.vst=iX3KahDRvnk_oLLhDi2j9dtXL2YdIi69pIbdM-EiUR6JQhPDfSxAhUk-Lp4Ab9azzej0norsWTwkXtCUQ43JHyg663Y2AHsu_G9NKdrqLxKQXwiGudBerMBG-IALxTVBpXE80Y6p01gAlMq325tOIge0nYeRuxe314Cvto8DgKmT4F0xs-QMtkk15iUDTnFXTnKsXXSVy9zwkVTR5-OfRCzqnD2tGfZUd3qtYn0D_xSzf6xST_X6mVaqFd8I_1DNSlVoQJ9FjJGR4uLqGYnxRQ; wwrtx.sid=CLp17Y7S1DAxv1dXMotNQ_zy9auezWJ7yeGUIBT7rvkmdlbBPsAjQ5Cz4KTK2LEM' +# -H 'origin: https://work.weixin.qq.com' +# -H 'pragma: no-cache' +# -H 'referer: https://work.weixin.qq.com/wework_admin/frame' +# -H 'sec-ch-ua: "Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"' +# -H 'sec-ch-ua-mobile: ?0' +# -H 'sec-ch-ua-platform: "macOS"' +# -H 'sec-fetch-dest: empty' +# -H 'sec-fetch-mode: cors' +# -H 'sec-fetch-site: same-origin' +# -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36' +# -H 'x-requested-with: XMLHttpRequest' +# --data-raw 'groupCorpWhite=&msgTypeStr=text&appid=5629500703629232&encrypt=0&textMsg%5Bcontent%5D=%E6%B5%8B%E8%AF%95%E5%95%8A&oper=Send&hasNoShareGroup=true&toallflag=0&range_v%5B0%5D%5Buserid%5D=1688858336390504&range_v%5B0%5D%5Busername%5D=%E5%94%90%E6%9C%89%E7%82%9C&msgtype=1' +# --compressed + +### + +### 获取access_token +### https://developer.work.weixin.qq.com/document/path/91039 +GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={{corpid}}&corpsecret={{corpsecret}} + +### 发送消息-不可用 +POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={{access_token}} +content-type: application/json + +{ + "touser": "1688858336390504", + "totag": "消息 | 笔记", + "msgtype": "text", + "agentid": 1000002, + "text": { + "content": "你的快递已到,请携带工卡前往邮件中心领取。\n出发前可查看邮件中心视频实况,聪明避开排队。" + }, + "safe": 0, + "enable_id_trans": 0, + "enable_duplicate_check": 0 +} + +### 发送消息[文本]-网页授权 +### 网页授权 +### https://work.weixin.qq.com/wework_admin/loginpage_wx?from=myhome +### appid在应用详情url可以找到 https://work.weixin.qq.com/wework_admin/frame#apps +### userid可以从cookie读取,wwrtx.vid +POST https://work.weixin.qq.com/wework_admin/message/sendmsg?lang=zh_CN&f=json&ajax=1&timeZoneInfo%5Bzone_offset%5D=-8& + random=0.9698314368566994 +authority: work.weixin.qq.com +cookie: {{cookie}} +origin: https://work.weixin.qq.com +referer: https://work.weixin.qq.com/wework_admin/frame +#x-requested-with: XMLHttpRequest +Content-Type: application/x-www-form-urlencoded + +groupCorpWhite = & +msgTypeStr = text & +appid = 5629500703629232 & +encrypt = 0 & +textMsg%5Bcontent%5D = %E6%B5%8B%E8%AF%95%E5%95%8A & +oper = Send & +hasNoShareGroup = true & +toallflag = 0 & +range_v%5B0%5D%5Buserid%5D = 1688858336390504 & +msgtype = 1 +